@pafi-dev/issuer 0.2.0 → 0.3.0-beta.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/README.md +58 -0
- package/dist/index.cjs +870 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +624 -79
- package/dist/index.d.ts +624 -79
- package/dist/index.js +862 -82
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,8 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AuthError: () => AuthError,
|
|
24
24
|
AuthService: () => AuthService,
|
|
25
|
+
BalanceAggregator: () => BalanceAggregator,
|
|
26
|
+
BurnIndexer: () => BurnIndexer,
|
|
25
27
|
DefaultPolicyEngine: () => DefaultPolicyEngine,
|
|
26
28
|
FeeManager: () => FeeManager,
|
|
27
29
|
InMemoryCursorStore: () => InMemoryCursorStore,
|
|
@@ -32,10 +34,16 @@ __export(index_exports, {
|
|
|
32
34
|
MintingGatewayError: () => MintingGatewayError,
|
|
33
35
|
NonceManager: () => NonceManager,
|
|
34
36
|
PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
|
|
37
|
+
PTRedeemError: () => PTRedeemError,
|
|
38
|
+
PTRedeemHandler: () => PTRedeemHandler,
|
|
39
|
+
PafiBackendClient: () => PafiBackendClient,
|
|
40
|
+
PafiBackendError: () => PafiBackendError,
|
|
35
41
|
PointIndexer: () => PointIndexer,
|
|
36
42
|
PrivateKeySigner: () => PrivateKeySigner,
|
|
37
43
|
RelayError: () => RelayError,
|
|
38
44
|
RelayService: () => RelayService,
|
|
45
|
+
TopUpRedemptionError: () => TopUpRedemptionError,
|
|
46
|
+
TopUpRedemptionHandler: () => TopUpRedemptionHandler,
|
|
39
47
|
authenticateRequest: () => authenticateRequest,
|
|
40
48
|
createIssuerService: () => createIssuerService,
|
|
41
49
|
createSubgraphNativeUsdtQuoter: () => createSubgraphNativeUsdtQuoter,
|
|
@@ -162,6 +170,54 @@ var MemoryPointLedger = class {
|
|
|
162
170
|
if (txHash) lock.txHash = txHash;
|
|
163
171
|
}
|
|
164
172
|
// -------------------------------------------------------------------------
|
|
173
|
+
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
174
|
+
// -------------------------------------------------------------------------
|
|
175
|
+
pendingCredits = /* @__PURE__ */ new Map();
|
|
176
|
+
nextCreditId = 1;
|
|
177
|
+
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
178
|
+
if (amount <= 0n) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"MemoryPointLedger: pending credit amount must be positive"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (durationMs <= 0) {
|
|
184
|
+
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
185
|
+
}
|
|
186
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
187
|
+
const lockId = `credit-${this.nextCreditId++}`;
|
|
188
|
+
const now = this.now();
|
|
189
|
+
this.pendingCredits.set(lockId, {
|
|
190
|
+
lockId,
|
|
191
|
+
userAddress: user,
|
|
192
|
+
amount,
|
|
193
|
+
tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
|
|
194
|
+
createdAt: now,
|
|
195
|
+
expiresAt: now + durationMs,
|
|
196
|
+
status: "PENDING"
|
|
197
|
+
});
|
|
198
|
+
return lockId;
|
|
199
|
+
}
|
|
200
|
+
async resolveCreditByBurnTx(lockId, txHash) {
|
|
201
|
+
const credit = this.pendingCredits.get(lockId);
|
|
202
|
+
if (!credit) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (credit.status === "RESOLVED") {
|
|
208
|
+
if (credit.txHash === txHash) return;
|
|
209
|
+
throw new Error(
|
|
210
|
+
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const token = normalizeToken(credit.tokenAddress);
|
|
214
|
+
const key = balanceKey(credit.userAddress, token);
|
|
215
|
+
const current = this.balances.get(key) ?? 0n;
|
|
216
|
+
this.balances.set(key, current + credit.amount);
|
|
217
|
+
credit.status = "RESOLVED";
|
|
218
|
+
credit.txHash = txHash;
|
|
219
|
+
}
|
|
220
|
+
// -------------------------------------------------------------------------
|
|
165
221
|
// Internal helpers
|
|
166
222
|
// -------------------------------------------------------------------------
|
|
167
223
|
/**
|
|
@@ -578,6 +634,7 @@ var RelayError = class extends Error {
|
|
|
578
634
|
};
|
|
579
635
|
|
|
580
636
|
// src/relay/relayService.ts
|
|
637
|
+
var import_viem5 = require("viem");
|
|
581
638
|
var import_core3 = require("@pafi-dev/core");
|
|
582
639
|
var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
583
640
|
var RelayService = class {
|
|
@@ -630,6 +687,11 @@ var RelayService = class {
|
|
|
630
687
|
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
631
688
|
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
632
689
|
* need manual review because the tx may still land).
|
|
690
|
+
*
|
|
691
|
+
* @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
|
|
692
|
+
* `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
|
|
693
|
+
* still needs to finalize Relayer v2 ABI before the replacements
|
|
694
|
+
* can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
|
|
633
695
|
*/
|
|
634
696
|
async submitMintAndSwap(params) {
|
|
635
697
|
if (this.simulateBeforeSubmit && this.provider) {
|
|
@@ -698,6 +760,154 @@ var RelayService = class {
|
|
|
698
760
|
);
|
|
699
761
|
}
|
|
700
762
|
}
|
|
763
|
+
// ==========================================================================
|
|
764
|
+
// v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
|
|
765
|
+
// ==========================================================================
|
|
766
|
+
//
|
|
767
|
+
// These two methods build unsigned `PartialUserOperation` payloads for
|
|
768
|
+
// the Frontend to sign (via Privy) and submit to the Bundler. The
|
|
769
|
+
// Issuer Backend no longer broadcasts — that's the Frontend's job.
|
|
770
|
+
//
|
|
771
|
+
// Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
|
|
772
|
+
// When SC delivers real ABIs, the imports swap but these method bodies
|
|
773
|
+
// stay the same (calldata encoder is ABI-driven).
|
|
774
|
+
// ==========================================================================
|
|
775
|
+
/**
|
|
776
|
+
* Build an unsigned UserOp for Scenario 1 (Mint).
|
|
777
|
+
*
|
|
778
|
+
* Flow:
|
|
779
|
+
* 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
|
|
780
|
+
* 2. Optionally append a PT fee transfer from user → feeRecipient
|
|
781
|
+
* (fee recovery happens on-chain via BatchExecutor, not via an
|
|
782
|
+
* operator wallet)
|
|
783
|
+
* 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
|
|
784
|
+
* 4. Return a `PartialUserOperation` ready for:
|
|
785
|
+
* - gas estimation (Bundler)
|
|
786
|
+
* - paymaster sponsorship (PAFI Backend)
|
|
787
|
+
* - user signature (Privy)
|
|
788
|
+
*/
|
|
789
|
+
prepareMint(params) {
|
|
790
|
+
if (!params.relayerAddress) {
|
|
791
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
|
|
792
|
+
}
|
|
793
|
+
if (!params.batchExecutorAddress) {
|
|
794
|
+
throw new RelayError(
|
|
795
|
+
"ENCODE_FAILED",
|
|
796
|
+
"prepareMint: batchExecutorAddress required"
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
if (!params.userAddress) {
|
|
800
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
|
|
801
|
+
}
|
|
802
|
+
let mintCallData;
|
|
803
|
+
try {
|
|
804
|
+
mintCallData = (0, import_viem5.encodeFunctionData)({
|
|
805
|
+
abi: import_core3.RELAYER_V2_ABI,
|
|
806
|
+
functionName: "mint",
|
|
807
|
+
args: [params.mintRequest, params.userSignature, params.issuerSignature]
|
|
808
|
+
});
|
|
809
|
+
} catch (err) {
|
|
810
|
+
throw new RelayError(
|
|
811
|
+
"ENCODE_FAILED",
|
|
812
|
+
`prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
|
|
813
|
+
err
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
const operations = [
|
|
817
|
+
{
|
|
818
|
+
target: params.relayerAddress,
|
|
819
|
+
value: 0n,
|
|
820
|
+
data: mintCallData
|
|
821
|
+
}
|
|
822
|
+
];
|
|
823
|
+
if (params.mintRequest.feeAmount > 0n) {
|
|
824
|
+
operations.push({
|
|
825
|
+
target: params.pointTokenAddress,
|
|
826
|
+
value: 0n,
|
|
827
|
+
data: (0, import_viem5.encodeFunctionData)({
|
|
828
|
+
abi: import_core3.POINT_TOKEN_V2_ABI,
|
|
829
|
+
functionName: "balanceOf",
|
|
830
|
+
// placeholder — real impl uses transfer
|
|
831
|
+
args: [params.mintRequest.feeRecipient]
|
|
832
|
+
})
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
return (0, import_core3.buildPartialUserOperation)({
|
|
836
|
+
sender: params.userAddress,
|
|
837
|
+
nonce: params.aaNonce,
|
|
838
|
+
operations,
|
|
839
|
+
gasLimits: {
|
|
840
|
+
callGasLimit: params.callGasLimit ?? 500000n,
|
|
841
|
+
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
842
|
+
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
|
|
848
|
+
*
|
|
849
|
+
* Two modes:
|
|
850
|
+
* - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
|
|
851
|
+
* via EIP-7702 delegation is the user, so no signature needed
|
|
852
|
+
* on-chain (the BurnConsent was already verified off-chain by
|
|
853
|
+
* the issuer backend before we got here)
|
|
854
|
+
* - `mode: 'burnWithSig'` — `PointToken.burnWithSig(consent, sig)`;
|
|
855
|
+
* used when the issuer hasn't verified the consent and the
|
|
856
|
+
* contract has to do it on-chain
|
|
857
|
+
*/
|
|
858
|
+
prepareBurn(params) {
|
|
859
|
+
if (!params.pointTokenAddress) {
|
|
860
|
+
throw new RelayError("ENCODE_FAILED", "prepareBurn: pointTokenAddress required");
|
|
861
|
+
}
|
|
862
|
+
if (!params.batchExecutorAddress) {
|
|
863
|
+
throw new RelayError(
|
|
864
|
+
"ENCODE_FAILED",
|
|
865
|
+
"prepareBurn: batchExecutorAddress required"
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
let burnCallData;
|
|
869
|
+
try {
|
|
870
|
+
if (params.mode === "burnWithSig") {
|
|
871
|
+
if (!params.burnConsent || !params.consentSignature) {
|
|
872
|
+
throw new Error("burnWithSig requires burnConsent + consentSignature");
|
|
873
|
+
}
|
|
874
|
+
burnCallData = (0, import_viem5.encodeFunctionData)({
|
|
875
|
+
abi: import_core3.POINT_TOKEN_V2_ABI,
|
|
876
|
+
functionName: "burnWithSig",
|
|
877
|
+
args: [params.burnConsent, params.consentSignature]
|
|
878
|
+
});
|
|
879
|
+
} else {
|
|
880
|
+
burnCallData = (0, import_viem5.encodeFunctionData)({
|
|
881
|
+
abi: import_core3.POINT_TOKEN_V2_ABI,
|
|
882
|
+
functionName: "burn",
|
|
883
|
+
args: [params.amount]
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
} catch (err) {
|
|
887
|
+
throw new RelayError(
|
|
888
|
+
"ENCODE_FAILED",
|
|
889
|
+
`prepareBurn: failed to encode burn call: ${errorMessage(err)}`,
|
|
890
|
+
err
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
const operations = [
|
|
894
|
+
{
|
|
895
|
+
target: params.pointTokenAddress,
|
|
896
|
+
value: 0n,
|
|
897
|
+
data: burnCallData
|
|
898
|
+
}
|
|
899
|
+
];
|
|
900
|
+
return (0, import_core3.buildPartialUserOperation)({
|
|
901
|
+
sender: params.userAddress,
|
|
902
|
+
nonce: params.aaNonce,
|
|
903
|
+
operations,
|
|
904
|
+
gasLimits: {
|
|
905
|
+
callGasLimit: params.callGasLimit ?? 300000n,
|
|
906
|
+
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
907
|
+
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
}
|
|
701
911
|
};
|
|
702
912
|
function errorMessage(err) {
|
|
703
913
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -708,84 +918,35 @@ var DEFAULT_GAS_UNITS = 500000n;
|
|
|
708
918
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
709
919
|
var FeeManager = class {
|
|
710
920
|
provider;
|
|
711
|
-
|
|
712
|
-
mintAndSwapGasUnits;
|
|
921
|
+
gasUnits;
|
|
713
922
|
gasPremiumBps;
|
|
714
|
-
|
|
715
|
-
rebalanceThresholdWei;
|
|
716
|
-
rebalanceUsdtAmount;
|
|
717
|
-
swapUsdtToNative;
|
|
923
|
+
quoteNativeToFee;
|
|
718
924
|
constructor(config) {
|
|
719
925
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
720
|
-
if (!config.
|
|
721
|
-
throw new Error("FeeManager:
|
|
722
|
-
if (!config.quoteNativeToUsdt)
|
|
723
|
-
throw new Error("FeeManager: quoteNativeToUsdt required");
|
|
926
|
+
if (!config.quoteNativeToFee)
|
|
927
|
+
throw new Error("FeeManager: quoteNativeToFee required");
|
|
724
928
|
this.provider = config.provider;
|
|
725
|
-
this.
|
|
726
|
-
this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
|
|
929
|
+
this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
|
|
727
930
|
this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
|
|
728
|
-
this.
|
|
729
|
-
if (config.rebalanceThresholdWei !== void 0) {
|
|
730
|
-
this.rebalanceThresholdWei = config.rebalanceThresholdWei;
|
|
731
|
-
}
|
|
732
|
-
if (config.rebalanceUsdtAmount !== void 0) {
|
|
733
|
-
this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
|
|
734
|
-
}
|
|
735
|
-
if (config.swapUsdtToNative) {
|
|
736
|
-
this.swapUsdtToNative = config.swapUsdtToNative;
|
|
737
|
-
}
|
|
738
|
-
const rebalanceFields = [
|
|
739
|
-
config.rebalanceThresholdWei,
|
|
740
|
-
config.rebalanceUsdtAmount,
|
|
741
|
-
config.swapUsdtToNative
|
|
742
|
-
];
|
|
743
|
-
const someSet = rebalanceFields.some((v) => v !== void 0);
|
|
744
|
-
const allSet = rebalanceFields.every((v) => v !== void 0);
|
|
745
|
-
if (someSet && !allSet) {
|
|
746
|
-
throw new Error(
|
|
747
|
-
"FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
|
|
748
|
-
);
|
|
749
|
-
}
|
|
931
|
+
this.quoteNativeToFee = config.quoteNativeToFee;
|
|
750
932
|
}
|
|
751
933
|
/**
|
|
752
|
-
* Estimate the
|
|
753
|
-
*
|
|
934
|
+
* Estimate the fee (in the caller's fee currency) to charge for the
|
|
935
|
+
* next sponsored UserOp:
|
|
936
|
+
*
|
|
937
|
+
* nativeCost = gasUnits × gasPrice
|
|
938
|
+
* withPremium = nativeCost × premiumBps / 10_000
|
|
939
|
+
* fee = quoteNativeToFee(withPremium)
|
|
754
940
|
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
941
|
+
* For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
|
|
942
|
+
* from the response, the name `estimateGasFee` is kept — but the
|
|
943
|
+
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
758
944
|
*/
|
|
759
945
|
async estimateGasFee() {
|
|
760
946
|
const gasPrice = await this.provider.getGasPrice();
|
|
761
|
-
const nativeCost = gasPrice * this.
|
|
947
|
+
const nativeCost = gasPrice * this.gasUnits;
|
|
762
948
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
763
|
-
return this.
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Check the operator's native balance and, if it has dropped below the
|
|
767
|
-
* configured threshold, trigger a USDT→native rebalance via the injected
|
|
768
|
-
* `swapUsdtToNative` function.
|
|
769
|
-
*
|
|
770
|
-
* Returns `true` if a rebalance was performed, `false` otherwise.
|
|
771
|
-
* Silently no-ops when rebalance is not configured.
|
|
772
|
-
*/
|
|
773
|
-
async rebalanceIfNeeded() {
|
|
774
|
-
if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
const operatorAddress = this.operatorWallet.account?.address;
|
|
778
|
-
if (!operatorAddress) {
|
|
779
|
-
throw new Error(
|
|
780
|
-
"FeeManager: operator wallet has no account attached \u2014 cannot read balance"
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
const balance = await this.provider.getBalance({ address: operatorAddress });
|
|
784
|
-
if (balance >= this.rebalanceThresholdWei) {
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
await this.swapUsdtToNative(this.rebalanceUsdtAmount);
|
|
788
|
-
return true;
|
|
949
|
+
return this.quoteNativeToFee(withPremium);
|
|
789
950
|
}
|
|
790
951
|
};
|
|
791
952
|
|
|
@@ -831,6 +992,12 @@ var MintingGateway = class {
|
|
|
831
992
|
this.now = config.now ?? (() => Date.now());
|
|
832
993
|
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
833
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* @deprecated Since 0.3.0 — will be renamed to `processMint()` once
|
|
997
|
+
* the SC team finalizes Relayer v2 ABI. The new flow drops the
|
|
998
|
+
* swap steps entirely (no more single-call mint+swap); users swap
|
|
999
|
+
* separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
|
|
1000
|
+
*/
|
|
834
1001
|
async processMintAndCashOut(request) {
|
|
835
1002
|
const { receiverConsent, receiverSignature } = request;
|
|
836
1003
|
if (!receiverConsent || !receiverSignature) {
|
|
@@ -1041,8 +1208,8 @@ var InMemoryCursorStore = class {
|
|
|
1041
1208
|
};
|
|
1042
1209
|
|
|
1043
1210
|
// src/indexer/pointIndexer.ts
|
|
1044
|
-
var
|
|
1045
|
-
var TRANSFER_EVENT = (0,
|
|
1211
|
+
var import_viem6 = require("viem");
|
|
1212
|
+
var TRANSFER_EVENT = (0, import_viem6.parseAbiItem)(
|
|
1046
1213
|
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1047
1214
|
);
|
|
1048
1215
|
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
@@ -1162,10 +1329,10 @@ var PointIndexer = class {
|
|
|
1162
1329
|
for (const log of logs) {
|
|
1163
1330
|
const args = log.args;
|
|
1164
1331
|
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1165
|
-
if ((0,
|
|
1332
|
+
if ((0, import_viem6.getAddress)(args.from) !== ZERO_ADDRESS) continue;
|
|
1166
1333
|
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1167
1334
|
out.push({
|
|
1168
|
-
to: (0,
|
|
1335
|
+
to: (0, import_viem6.getAddress)(args.to),
|
|
1169
1336
|
amount: args.value,
|
|
1170
1337
|
blockNumber: log.blockNumber,
|
|
1171
1338
|
txHash: log.transactionHash,
|
|
@@ -1220,8 +1387,159 @@ function pickMatchingLock(locks, amount) {
|
|
|
1220
1387
|
return best;
|
|
1221
1388
|
}
|
|
1222
1389
|
|
|
1390
|
+
// src/indexer/burnIndexer.ts
|
|
1391
|
+
var import_viem7 = require("viem");
|
|
1392
|
+
var TRANSFER_EVENT2 = (0, import_viem7.parseAbiItem)(
|
|
1393
|
+
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1394
|
+
);
|
|
1395
|
+
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
1396
|
+
var DEFAULT_CONFIRMATIONS2 = 3;
|
|
1397
|
+
var DEFAULT_BATCH_SIZE2 = 2000n;
|
|
1398
|
+
var DEFAULT_POLL_INTERVAL_MS2 = 5e3;
|
|
1399
|
+
var BurnIndexer = class {
|
|
1400
|
+
provider;
|
|
1401
|
+
pointTokenAddress;
|
|
1402
|
+
ledger;
|
|
1403
|
+
cursorStore;
|
|
1404
|
+
startBlock;
|
|
1405
|
+
confirmations;
|
|
1406
|
+
batchSize;
|
|
1407
|
+
pollIntervalMs;
|
|
1408
|
+
/**
|
|
1409
|
+
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1410
|
+
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1411
|
+
* ledger's query path.
|
|
1412
|
+
*
|
|
1413
|
+
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1414
|
+
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1415
|
+
* incrementing IDs so callers with the memory ledger must provide a
|
|
1416
|
+
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1417
|
+
* their `pending_credits` table.
|
|
1418
|
+
*/
|
|
1419
|
+
matchLockId = async () => void 0;
|
|
1420
|
+
running = false;
|
|
1421
|
+
timer;
|
|
1422
|
+
constructor(config) {
|
|
1423
|
+
if (!config.provider) throw new Error("BurnIndexer: provider required");
|
|
1424
|
+
if (!config.pointTokenAddress)
|
|
1425
|
+
throw new Error("BurnIndexer: pointTokenAddress required");
|
|
1426
|
+
if (!config.ledger) throw new Error("BurnIndexer: ledger required");
|
|
1427
|
+
this.provider = config.provider;
|
|
1428
|
+
this.pointTokenAddress = config.pointTokenAddress;
|
|
1429
|
+
this.ledger = config.ledger;
|
|
1430
|
+
this.cursorStore = config.cursorStore ?? new InMemoryCursorStore();
|
|
1431
|
+
this.startBlock = config.fromBlock ?? 0n;
|
|
1432
|
+
this.confirmations = BigInt(
|
|
1433
|
+
config.confirmations ?? DEFAULT_CONFIRMATIONS2
|
|
1434
|
+
);
|
|
1435
|
+
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1436
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
1437
|
+
}
|
|
1438
|
+
start() {
|
|
1439
|
+
if (this.running) return;
|
|
1440
|
+
this.running = true;
|
|
1441
|
+
void this.tick();
|
|
1442
|
+
}
|
|
1443
|
+
stop() {
|
|
1444
|
+
this.running = false;
|
|
1445
|
+
if (this.timer) {
|
|
1446
|
+
clearTimeout(this.timer);
|
|
1447
|
+
this.timer = void 0;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async tick() {
|
|
1451
|
+
if (!this.running) return;
|
|
1452
|
+
try {
|
|
1453
|
+
const latest = await this.provider.getBlockNumber();
|
|
1454
|
+
const safeHead = latest - this.confirmations;
|
|
1455
|
+
if (safeHead < 0n) {
|
|
1456
|
+
this.scheduleNext();
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const stored = await this.cursorStore.load();
|
|
1460
|
+
const from = stored ?? this.startBlock;
|
|
1461
|
+
if (from > safeHead) {
|
|
1462
|
+
this.scheduleNext();
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
await this.processBlockRange(from, safeHead);
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
this.scheduleNext();
|
|
1469
|
+
}
|
|
1470
|
+
scheduleNext() {
|
|
1471
|
+
if (!this.running) return;
|
|
1472
|
+
this.timer = setTimeout(() => void this.tick(), this.pollIntervalMs);
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Scan `[from, to]` inclusive for burn events. Callers can drive this
|
|
1476
|
+
* directly to backfill a specific range without `start()`. Cursor is
|
|
1477
|
+
* advanced to `to + 1` on completion.
|
|
1478
|
+
*/
|
|
1479
|
+
async processBlockRange(from, to) {
|
|
1480
|
+
if (from > to) return;
|
|
1481
|
+
let cursor = from;
|
|
1482
|
+
while (cursor <= to) {
|
|
1483
|
+
const chunkEnd = cursor + this.batchSize - 1n > to ? to : cursor + this.batchSize - 1n;
|
|
1484
|
+
const logs = await this.provider.getLogs({
|
|
1485
|
+
address: this.pointTokenAddress,
|
|
1486
|
+
event: TRANSFER_EVENT2,
|
|
1487
|
+
args: { to: ZERO_ADDRESS2 },
|
|
1488
|
+
// filter: burn = transfer to zero
|
|
1489
|
+
fromBlock: cursor,
|
|
1490
|
+
toBlock: chunkEnd
|
|
1491
|
+
});
|
|
1492
|
+
const events = this.decodeBurnEvents(logs);
|
|
1493
|
+
events.sort((a, b) => {
|
|
1494
|
+
if (a.blockNumber !== b.blockNumber) {
|
|
1495
|
+
return a.blockNumber < b.blockNumber ? -1 : 1;
|
|
1496
|
+
}
|
|
1497
|
+
return a.logIndex - b.logIndex;
|
|
1498
|
+
});
|
|
1499
|
+
for (const evt of events) {
|
|
1500
|
+
await this.finalize(evt);
|
|
1501
|
+
}
|
|
1502
|
+
await this.cursorStore.save(chunkEnd + 1n);
|
|
1503
|
+
cursor = chunkEnd + 1n;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
decodeBurnEvents(logs) {
|
|
1507
|
+
const out = [];
|
|
1508
|
+
for (const log of logs) {
|
|
1509
|
+
const args = log.args;
|
|
1510
|
+
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1511
|
+
if ((0, import_viem7.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
|
|
1512
|
+
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1513
|
+
out.push({
|
|
1514
|
+
from: (0, import_viem7.getAddress)(args.from),
|
|
1515
|
+
amount: args.value,
|
|
1516
|
+
blockNumber: log.blockNumber,
|
|
1517
|
+
txHash: log.transactionHash,
|
|
1518
|
+
logIndex: log.logIndex ?? 0
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
return out;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Resolve a matching pending credit for this burn event and call
|
|
1525
|
+
* `ledger.resolveCreditByBurnTx(lockId, txHash)`. If no match found,
|
|
1526
|
+
* log + skip.
|
|
1527
|
+
*/
|
|
1528
|
+
async finalize(evt) {
|
|
1529
|
+
const lockId = await this.matchLockId(evt);
|
|
1530
|
+
if (!lockId) return;
|
|
1531
|
+
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1223
1541
|
// src/api/handlers.ts
|
|
1224
|
-
var
|
|
1542
|
+
var import_viem8 = require("viem");
|
|
1225
1543
|
var import_core5 = require("@pafi-dev/core");
|
|
1226
1544
|
var IssuerApiHandlers = class {
|
|
1227
1545
|
authService;
|
|
@@ -1239,6 +1557,7 @@ var IssuerApiHandlers = class {
|
|
|
1239
1557
|
defaultToken;
|
|
1240
1558
|
chainId;
|
|
1241
1559
|
contracts;
|
|
1560
|
+
pafiWebUrl;
|
|
1242
1561
|
feeManager;
|
|
1243
1562
|
poolsProvider;
|
|
1244
1563
|
constructor(config) {
|
|
@@ -1252,11 +1571,12 @@ var IssuerApiHandlers = class {
|
|
|
1252
1571
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1253
1572
|
);
|
|
1254
1573
|
}
|
|
1255
|
-
const normalized = raw.map((a) => (0,
|
|
1574
|
+
const normalized = raw.map((a) => (0, import_viem8.getAddress)(a));
|
|
1256
1575
|
this.supportedTokens = new Set(normalized);
|
|
1257
1576
|
this.defaultToken = normalized[0];
|
|
1258
1577
|
this.chainId = config.chainId;
|
|
1259
1578
|
this.contracts = config.contracts;
|
|
1579
|
+
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1260
1580
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1261
1581
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1262
1582
|
}
|
|
@@ -1292,7 +1612,16 @@ var IssuerApiHandlers = class {
|
|
|
1292
1612
|
`handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
|
|
1293
1613
|
);
|
|
1294
1614
|
}
|
|
1295
|
-
|
|
1615
|
+
const contracts = {
|
|
1616
|
+
...this.contracts,
|
|
1617
|
+
pointTokens: Array.from(this.supportedTokens)
|
|
1618
|
+
};
|
|
1619
|
+
const response = {
|
|
1620
|
+
chainId: this.chainId,
|
|
1621
|
+
contracts
|
|
1622
|
+
};
|
|
1623
|
+
if (this.pafiWebUrl) response.pafiWebUrl = this.pafiWebUrl;
|
|
1624
|
+
return response;
|
|
1296
1625
|
}
|
|
1297
1626
|
/** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
|
|
1298
1627
|
async handleGasFee() {
|
|
@@ -1343,14 +1672,14 @@ var IssuerApiHandlers = class {
|
|
|
1343
1672
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1344
1673
|
);
|
|
1345
1674
|
}
|
|
1346
|
-
const normalizedAuthed = (0,
|
|
1347
|
-
const normalizedRequest = (0,
|
|
1675
|
+
const normalizedAuthed = (0, import_viem8.getAddress)(userAddress);
|
|
1676
|
+
const normalizedRequest = (0, import_viem8.getAddress)(request.userAddress);
|
|
1348
1677
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1349
1678
|
throw new Error(
|
|
1350
1679
|
"handleUser: request userAddress must match authenticated user"
|
|
1351
1680
|
);
|
|
1352
1681
|
}
|
|
1353
|
-
const pointToken = (0,
|
|
1682
|
+
const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
|
|
1354
1683
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1355
1684
|
throw new Error(
|
|
1356
1685
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
@@ -1393,7 +1722,7 @@ var IssuerApiHandlers = class {
|
|
|
1393
1722
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1394
1723
|
);
|
|
1395
1724
|
}
|
|
1396
|
-
const pointToken = (0,
|
|
1725
|
+
const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
|
|
1397
1726
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1398
1727
|
throw new Error(
|
|
1399
1728
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
@@ -1418,8 +1747,14 @@ var IssuerApiHandlers = class {
|
|
|
1418
1747
|
/**
|
|
1419
1748
|
* `POST /claim-and-swap`
|
|
1420
1749
|
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1750
|
+
* @deprecated Since 0.3.0 — the single-call mint-then-swap flow is
|
|
1751
|
+
* retired in v1.4. Use the new `handleClaim()` (mint only) and let
|
|
1752
|
+
* the user swap separately on PAFI Web. See
|
|
1753
|
+
* [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
|
|
1754
|
+
* removed in 2.0.
|
|
1755
|
+
*
|
|
1756
|
+
* Legacy behavior: the terminal handler forwards the verified
|
|
1757
|
+
* consent to the MintingGateway, which runs the 11-step flow.
|
|
1423
1758
|
*/
|
|
1424
1759
|
async handleClaimAndSwap(userAddress, request) {
|
|
1425
1760
|
if (request.chainId !== this.chainId) {
|
|
@@ -1427,14 +1762,14 @@ var IssuerApiHandlers = class {
|
|
|
1427
1762
|
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1428
1763
|
);
|
|
1429
1764
|
}
|
|
1430
|
-
const pointToken = (0,
|
|
1765
|
+
const pointToken = (0, import_viem8.getAddress)(request.pointTokenAddress);
|
|
1431
1766
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1432
1767
|
throw new Error(
|
|
1433
1768
|
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1434
1769
|
);
|
|
1435
1770
|
}
|
|
1436
1771
|
const result = await this.gateway.processMintAndCashOut({
|
|
1437
|
-
userAddress: (0,
|
|
1772
|
+
userAddress: (0, import_viem8.getAddress)(userAddress),
|
|
1438
1773
|
pointTokenAddress: pointToken,
|
|
1439
1774
|
chainId: request.chainId,
|
|
1440
1775
|
domain: request.domain,
|
|
@@ -1454,6 +1789,183 @@ var IssuerApiHandlers = class {
|
|
|
1454
1789
|
}
|
|
1455
1790
|
};
|
|
1456
1791
|
|
|
1792
|
+
// src/api/handlers/ptRedeemHandler.ts
|
|
1793
|
+
var import_viem9 = require("viem");
|
|
1794
|
+
var import_core6 = require("@pafi-dev/core");
|
|
1795
|
+
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1796
|
+
var PTRedeemError = class extends Error {
|
|
1797
|
+
constructor(code, message) {
|
|
1798
|
+
super(message);
|
|
1799
|
+
this.code = code;
|
|
1800
|
+
this.name = "PTRedeemError";
|
|
1801
|
+
}
|
|
1802
|
+
code;
|
|
1803
|
+
};
|
|
1804
|
+
var PTRedeemHandler = class {
|
|
1805
|
+
ledger;
|
|
1806
|
+
relayService;
|
|
1807
|
+
pointTokenAddress;
|
|
1808
|
+
batchExecutorAddress;
|
|
1809
|
+
chainId;
|
|
1810
|
+
domain;
|
|
1811
|
+
redeemLockDurationMs;
|
|
1812
|
+
now;
|
|
1813
|
+
constructor(config) {
|
|
1814
|
+
if (!config.ledger.reservePendingCredit) {
|
|
1815
|
+
throw new PTRedeemError(
|
|
1816
|
+
"LEDGER_NOT_SUPPORTED",
|
|
1817
|
+
"PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
this.ledger = config.ledger;
|
|
1821
|
+
this.relayService = config.relayService;
|
|
1822
|
+
this.pointTokenAddress = (0, import_viem9.getAddress)(config.pointTokenAddress);
|
|
1823
|
+
this.batchExecutorAddress = (0, import_viem9.getAddress)(config.batchExecutorAddress);
|
|
1824
|
+
this.chainId = config.chainId;
|
|
1825
|
+
this.domain = config.domain;
|
|
1826
|
+
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1827
|
+
this.now = config.now ?? (() => Date.now());
|
|
1828
|
+
}
|
|
1829
|
+
async handle(request) {
|
|
1830
|
+
if (request.amount <= 0n) {
|
|
1831
|
+
throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
|
|
1832
|
+
}
|
|
1833
|
+
if (request.consent.amount !== request.amount) {
|
|
1834
|
+
throw new PTRedeemError(
|
|
1835
|
+
"AMOUNT_MISMATCH",
|
|
1836
|
+
`consent.amount (${request.consent.amount}) must match request.amount (${request.amount})`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
|
|
1840
|
+
if (request.consent.deadline <= nowSeconds) {
|
|
1841
|
+
throw new PTRedeemError(
|
|
1842
|
+
"EXPIRED_CONSENT",
|
|
1843
|
+
`consent deadline (${request.consent.deadline}) already passed`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
const verification = await (0, import_core6.verifyBurnConsent)(
|
|
1847
|
+
{
|
|
1848
|
+
name: this.domain.name,
|
|
1849
|
+
chainId: this.chainId,
|
|
1850
|
+
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1851
|
+
},
|
|
1852
|
+
request.consent,
|
|
1853
|
+
request.consentSignature,
|
|
1854
|
+
request.userAddress
|
|
1855
|
+
);
|
|
1856
|
+
if (!verification.isValid) {
|
|
1857
|
+
throw new PTRedeemError(
|
|
1858
|
+
"SIGNATURE_MISMATCH",
|
|
1859
|
+
`signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
const lockId = await this.ledger.reservePendingCredit(
|
|
1863
|
+
request.userAddress,
|
|
1864
|
+
request.amount,
|
|
1865
|
+
this.redeemLockDurationMs,
|
|
1866
|
+
this.pointTokenAddress
|
|
1867
|
+
);
|
|
1868
|
+
const userOp = this.relayService.prepareBurn({
|
|
1869
|
+
mode: "burnWithSig",
|
|
1870
|
+
userAddress: request.userAddress,
|
|
1871
|
+
aaNonce: request.aaNonce,
|
|
1872
|
+
pointTokenAddress: this.pointTokenAddress,
|
|
1873
|
+
batchExecutorAddress: this.batchExecutorAddress,
|
|
1874
|
+
burnConsent: request.consent,
|
|
1875
|
+
consentSignature: parseSigStruct(request.consentSignature)
|
|
1876
|
+
});
|
|
1877
|
+
return {
|
|
1878
|
+
lockId,
|
|
1879
|
+
userOp,
|
|
1880
|
+
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
function parseSigStruct(serialized) {
|
|
1885
|
+
const raw = serialized.slice(2);
|
|
1886
|
+
if (raw.length !== 130) {
|
|
1887
|
+
throw new PTRedeemError(
|
|
1888
|
+
"INVALID_CONSENT",
|
|
1889
|
+
`signature must be 65 bytes, got ${raw.length / 2}`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
const r = `0x${raw.slice(0, 64)}`;
|
|
1893
|
+
const s = `0x${raw.slice(64, 128)}`;
|
|
1894
|
+
const v = parseInt(raw.slice(128, 130), 16);
|
|
1895
|
+
return { v, r, s };
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1899
|
+
var import_viem10 = require("viem");
|
|
1900
|
+
var import_core7 = require("@pafi-dev/core");
|
|
1901
|
+
var TopUpRedemptionError = class extends Error {
|
|
1902
|
+
constructor(code, message) {
|
|
1903
|
+
super(message);
|
|
1904
|
+
this.code = code;
|
|
1905
|
+
this.name = "TopUpRedemptionError";
|
|
1906
|
+
}
|
|
1907
|
+
code;
|
|
1908
|
+
};
|
|
1909
|
+
var TopUpRedemptionHandler = class {
|
|
1910
|
+
ledger;
|
|
1911
|
+
ptRedeemHandler;
|
|
1912
|
+
provider;
|
|
1913
|
+
pointTokenAddress;
|
|
1914
|
+
constructor(config) {
|
|
1915
|
+
this.ledger = config.ledger;
|
|
1916
|
+
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1917
|
+
this.provider = config.provider;
|
|
1918
|
+
this.pointTokenAddress = (0, import_viem10.getAddress)(config.pointTokenAddress);
|
|
1919
|
+
}
|
|
1920
|
+
async handle(request) {
|
|
1921
|
+
const offChainBalance = await this.ledger.getBalance(
|
|
1922
|
+
request.userAddress,
|
|
1923
|
+
this.pointTokenAddress
|
|
1924
|
+
);
|
|
1925
|
+
if (offChainBalance >= request.requiredAmount) {
|
|
1926
|
+
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1927
|
+
}
|
|
1928
|
+
const shortfall = request.requiredAmount - offChainBalance;
|
|
1929
|
+
const onChainBalance = await (0, import_core7.getPointTokenBalance)(
|
|
1930
|
+
this.provider,
|
|
1931
|
+
this.pointTokenAddress,
|
|
1932
|
+
request.userAddress
|
|
1933
|
+
);
|
|
1934
|
+
if (onChainBalance < shortfall) {
|
|
1935
|
+
return {
|
|
1936
|
+
action: "INSUFFICIENT_ONCHAIN",
|
|
1937
|
+
offChainBalance,
|
|
1938
|
+
onChainBalance,
|
|
1939
|
+
shortfall
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
if (request.redeemRequest.consent.amount < shortfall) {
|
|
1943
|
+
throw new TopUpRedemptionError(
|
|
1944
|
+
"CONSENT_AMOUNT_TOO_LOW",
|
|
1945
|
+
`consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
if (request.redeemRequest.consent.amount !== shortfall) {
|
|
1949
|
+
throw new TopUpRedemptionError(
|
|
1950
|
+
"CONSENT_AMOUNT_TOO_LOW",
|
|
1951
|
+
`consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
const redeem = await this.ptRedeemHandler.handle({
|
|
1955
|
+
userAddress: request.userAddress,
|
|
1956
|
+
amount: shortfall,
|
|
1957
|
+
consent: request.redeemRequest.consent,
|
|
1958
|
+
consentSignature: request.redeemRequest.consentSignature,
|
|
1959
|
+
aaNonce: request.redeemRequest.aaNonce
|
|
1960
|
+
});
|
|
1961
|
+
return {
|
|
1962
|
+
action: "TOP_UP_STARTED",
|
|
1963
|
+
shortfall,
|
|
1964
|
+
redeem
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1457
1969
|
// src/pools/subgraphPoolsProvider.ts
|
|
1458
1970
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1459
1971
|
var POOL_QUERY = `
|
|
@@ -1665,8 +2177,274 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1665
2177
|
return BigInt(whole + padded);
|
|
1666
2178
|
}
|
|
1667
2179
|
|
|
2180
|
+
// src/balance/balanceAggregator.ts
|
|
2181
|
+
var import_core8 = require("@pafi-dev/core");
|
|
2182
|
+
var BalanceAggregator = class {
|
|
2183
|
+
provider;
|
|
2184
|
+
ledger;
|
|
2185
|
+
constructor(config) {
|
|
2186
|
+
if (!config.provider) {
|
|
2187
|
+
throw new Error("BalanceAggregator: provider is required");
|
|
2188
|
+
}
|
|
2189
|
+
if (!config.ledger) {
|
|
2190
|
+
throw new Error("BalanceAggregator: ledger is required");
|
|
2191
|
+
}
|
|
2192
|
+
this.provider = config.provider;
|
|
2193
|
+
this.ledger = config.ledger;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Combined balance for a single (user, token) pair. Fetches off-chain
|
|
2197
|
+
* + on-chain in parallel.
|
|
2198
|
+
*/
|
|
2199
|
+
async getCombinedBalance(user, pointToken) {
|
|
2200
|
+
const [offChain, onChain] = await Promise.all([
|
|
2201
|
+
this.ledger.getBalance(user, pointToken),
|
|
2202
|
+
(0, import_core8.getPointTokenBalance)(this.provider, pointToken, user)
|
|
2203
|
+
]);
|
|
2204
|
+
return {
|
|
2205
|
+
offChain,
|
|
2206
|
+
onChain,
|
|
2207
|
+
total: offChain + onChain
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Combined balance for multiple tokens owned by the same user. Runs
|
|
2212
|
+
* all lookups in parallel. Returns a Map keyed by the token address
|
|
2213
|
+
* (same casing as supplied — caller should normalize if needed).
|
|
2214
|
+
*/
|
|
2215
|
+
async getCombinedBalanceMulti(user, pointTokens) {
|
|
2216
|
+
const entries = await Promise.all(
|
|
2217
|
+
pointTokens.map(async (token) => {
|
|
2218
|
+
const balance = await this.getCombinedBalance(user, token);
|
|
2219
|
+
return [token, balance];
|
|
2220
|
+
})
|
|
2221
|
+
);
|
|
2222
|
+
return new Map(entries);
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
// src/pafi-backend/types.ts
|
|
2227
|
+
var PafiBackendError = class extends Error {
|
|
2228
|
+
constructor(code, message, httpStatus, details, opts) {
|
|
2229
|
+
super(message);
|
|
2230
|
+
this.code = code;
|
|
2231
|
+
this.httpStatus = httpStatus;
|
|
2232
|
+
this.details = details;
|
|
2233
|
+
this.name = "PafiBackendError";
|
|
2234
|
+
if (opts?.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
|
|
2235
|
+
if (opts?.safeToRetry !== void 0) this.serverSafeToRetry = opts.safeToRetry;
|
|
2236
|
+
}
|
|
2237
|
+
code;
|
|
2238
|
+
httpStatus;
|
|
2239
|
+
details;
|
|
2240
|
+
/**
|
|
2241
|
+
* Seconds to wait before retry. Populated from the server body
|
|
2242
|
+
* (e.g. rate limit returns the number of seconds until UTC midnight).
|
|
2243
|
+
*/
|
|
2244
|
+
retryAfter;
|
|
2245
|
+
/**
|
|
2246
|
+
* `safeToRetry` as reported by the server body. Prefer this over the
|
|
2247
|
+
* code-based heuristic when available — the server knows more about
|
|
2248
|
+
* whether the same request will succeed on retry.
|
|
2249
|
+
*/
|
|
2250
|
+
serverSafeToRetry;
|
|
2251
|
+
/**
|
|
2252
|
+
* Whether the caller can safely retry the same request.
|
|
2253
|
+
*
|
|
2254
|
+
* If the server provided `safeToRetry` in the body, trust that.
|
|
2255
|
+
* Otherwise fall back to a code-based heuristic.
|
|
2256
|
+
*/
|
|
2257
|
+
get safeToRetry() {
|
|
2258
|
+
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
2259
|
+
switch (this.code) {
|
|
2260
|
+
case "PAYMASTER_UNAVAILABLE":
|
|
2261
|
+
case "PAYMASTER_TIMEOUT":
|
|
2262
|
+
case "RATE_LIMITER_UNAVAILABLE":
|
|
2263
|
+
case "INTERNAL_ERROR":
|
|
2264
|
+
case "TIMEOUT":
|
|
2265
|
+
case "NETWORK_ERROR":
|
|
2266
|
+
return true;
|
|
2267
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
2268
|
+
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
2269
|
+
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
2270
|
+
return true;
|
|
2271
|
+
// after retryAfter
|
|
2272
|
+
default:
|
|
2273
|
+
return false;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
|
|
2278
|
+
// src/pafi-backend/pafiBackendClient.ts
|
|
2279
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2280
|
+
var RETRY_DEFAULTS = {
|
|
2281
|
+
maxAttempts: 1,
|
|
2282
|
+
initialDelayMs: 500,
|
|
2283
|
+
maxDelayMs: 1e4,
|
|
2284
|
+
maxRetryAfterMs: 3e4
|
|
2285
|
+
};
|
|
2286
|
+
var PafiBackendClient = class {
|
|
2287
|
+
url;
|
|
2288
|
+
issuerId;
|
|
2289
|
+
apiKey;
|
|
2290
|
+
fetchImpl;
|
|
2291
|
+
timeoutMs;
|
|
2292
|
+
retry;
|
|
2293
|
+
constructor(config) {
|
|
2294
|
+
if (!config.url) {
|
|
2295
|
+
throw new Error("PafiBackendClient: url is required");
|
|
2296
|
+
}
|
|
2297
|
+
if (!config.issuerId) {
|
|
2298
|
+
throw new Error("PafiBackendClient: issuerId is required");
|
|
2299
|
+
}
|
|
2300
|
+
if (!config.apiKey) {
|
|
2301
|
+
throw new Error("PafiBackendClient: apiKey is required");
|
|
2302
|
+
}
|
|
2303
|
+
this.url = config.url.replace(/\/+$/, "");
|
|
2304
|
+
this.issuerId = config.issuerId;
|
|
2305
|
+
this.apiKey = config.apiKey;
|
|
2306
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
2307
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2308
|
+
this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
|
|
2309
|
+
if (!this.fetchImpl) {
|
|
2310
|
+
throw new Error(
|
|
2311
|
+
"PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
if (this.retry.maxAttempts < 1) {
|
|
2315
|
+
throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Request paymaster sponsorship for a pre-built UserOperation.
|
|
2320
|
+
* See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
|
|
2321
|
+
*
|
|
2322
|
+
* Retries automatically on transient failures (5xx, timeouts, network
|
|
2323
|
+
* errors, and errors the server flags with `safeToRetry: true`) up to
|
|
2324
|
+
* `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
|
|
2325
|
+
*
|
|
2326
|
+
* @throws PafiBackendError on final failure after exhausting retries
|
|
2327
|
+
*/
|
|
2328
|
+
async requestSponsorship(req) {
|
|
2329
|
+
return this.postWithRetry(
|
|
2330
|
+
"/paymaster/sponsor",
|
|
2331
|
+
req
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
// -------------------------------------------------------------------------
|
|
2335
|
+
// Internals
|
|
2336
|
+
// -------------------------------------------------------------------------
|
|
2337
|
+
async postWithRetry(path, body) {
|
|
2338
|
+
let lastError;
|
|
2339
|
+
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
2340
|
+
try {
|
|
2341
|
+
return await this.post(path, body);
|
|
2342
|
+
} catch (err) {
|
|
2343
|
+
if (!(err instanceof PafiBackendError)) throw err;
|
|
2344
|
+
lastError = err;
|
|
2345
|
+
const isLastAttempt = attempt >= this.retry.maxAttempts;
|
|
2346
|
+
if (isLastAttempt || !err.safeToRetry) throw err;
|
|
2347
|
+
const delay = this.computeBackoff(attempt, err.retryAfter);
|
|
2348
|
+
if (delay === null) throw err;
|
|
2349
|
+
await this.sleep(delay);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
throw lastError;
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Pick the delay before the next retry.
|
|
2356
|
+
* - If the server sent `retryAfter` (seconds), honor it (capped by
|
|
2357
|
+
* `maxRetryAfterMs`) — returns null if the server wait exceeds the
|
|
2358
|
+
* cap, signalling the caller should give up.
|
|
2359
|
+
* - Otherwise: exponential backoff with ±20% jitter, capped at
|
|
2360
|
+
* `maxDelayMs`.
|
|
2361
|
+
*/
|
|
2362
|
+
computeBackoff(attempt, retryAfter) {
|
|
2363
|
+
if (retryAfter !== void 0) {
|
|
2364
|
+
const serverMs = retryAfter * 1e3;
|
|
2365
|
+
if (serverMs > this.retry.maxRetryAfterMs) return null;
|
|
2366
|
+
return serverMs;
|
|
2367
|
+
}
|
|
2368
|
+
const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
|
|
2369
|
+
const capped = Math.min(exp, this.retry.maxDelayMs);
|
|
2370
|
+
const jitter = capped * (0.8 + Math.random() * 0.4);
|
|
2371
|
+
return Math.round(jitter);
|
|
2372
|
+
}
|
|
2373
|
+
sleep(ms) {
|
|
2374
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2375
|
+
}
|
|
2376
|
+
async post(path, body) {
|
|
2377
|
+
const controller = new AbortController();
|
|
2378
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2379
|
+
let response;
|
|
2380
|
+
try {
|
|
2381
|
+
response = await this.fetchImpl(`${this.url}${path}`, {
|
|
2382
|
+
method: "POST",
|
|
2383
|
+
headers: {
|
|
2384
|
+
"Content-Type": "application/json",
|
|
2385
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2386
|
+
"X-Issuer-Id": this.issuerId
|
|
2387
|
+
},
|
|
2388
|
+
body: JSON.stringify(body, this.bigintReplacer),
|
|
2389
|
+
signal: controller.signal
|
|
2390
|
+
});
|
|
2391
|
+
} catch (err) {
|
|
2392
|
+
if (err.name === "AbortError") {
|
|
2393
|
+
throw new PafiBackendError(
|
|
2394
|
+
"TIMEOUT",
|
|
2395
|
+
`PAFI Backend request timed out after ${this.timeoutMs}ms`,
|
|
2396
|
+
0
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
throw new PafiBackendError(
|
|
2400
|
+
"NETWORK_ERROR",
|
|
2401
|
+
`PAFI Backend unreachable: ${err.message}`,
|
|
2402
|
+
0
|
|
2403
|
+
);
|
|
2404
|
+
} finally {
|
|
2405
|
+
clearTimeout(timeoutId);
|
|
2406
|
+
}
|
|
2407
|
+
const text = await response.text();
|
|
2408
|
+
if (!response.ok) {
|
|
2409
|
+
let code = "INTERNAL_ERROR";
|
|
2410
|
+
let message = text || response.statusText;
|
|
2411
|
+
let details;
|
|
2412
|
+
let retryAfter;
|
|
2413
|
+
let serverSafeToRetry;
|
|
2414
|
+
try {
|
|
2415
|
+
const parsed = JSON.parse(text);
|
|
2416
|
+
code = parsed.code ?? code;
|
|
2417
|
+
message = parsed.message ?? message;
|
|
2418
|
+
details = parsed.details;
|
|
2419
|
+
if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
|
|
2420
|
+
if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
|
|
2421
|
+
} catch {
|
|
2422
|
+
}
|
|
2423
|
+
throw new PafiBackendError(code, message, response.status, details, {
|
|
2424
|
+
...retryAfter !== void 0 ? { retryAfter } : {},
|
|
2425
|
+
...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
return JSON.parse(text, this.bigintReviver);
|
|
2429
|
+
}
|
|
2430
|
+
/** JSON replacer that stringifies bigints. Paired with bigintReviver. */
|
|
2431
|
+
bigintReplacer = (_key, value) => {
|
|
2432
|
+
return typeof value === "bigint" ? value.toString() : value;
|
|
2433
|
+
};
|
|
2434
|
+
/**
|
|
2435
|
+
* JSON reviver that coerces specific numeric-string fields back to
|
|
2436
|
+
* bigint. The server must send these fields as decimal strings.
|
|
2437
|
+
*/
|
|
2438
|
+
bigintReviver = (key, value) => {
|
|
2439
|
+
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)) {
|
|
2440
|
+
return BigInt(value);
|
|
2441
|
+
}
|
|
2442
|
+
return value;
|
|
2443
|
+
};
|
|
2444
|
+
};
|
|
2445
|
+
|
|
1668
2446
|
// src/config.ts
|
|
1669
|
-
var
|
|
2447
|
+
var import_viem11 = require("viem");
|
|
1670
2448
|
function createIssuerService(config) {
|
|
1671
2449
|
if (!config.provider) {
|
|
1672
2450
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -1692,7 +2470,7 @@ function createIssuerService(config) {
|
|
|
1692
2470
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
1693
2471
|
);
|
|
1694
2472
|
}
|
|
1695
|
-
const tokenAddresses = rawAddresses.map((a) => (0,
|
|
2473
|
+
const tokenAddresses = rawAddresses.map((a) => (0, import_viem11.getAddress)(a));
|
|
1696
2474
|
const ledger = config.ledger ?? new MemoryPointLedger();
|
|
1697
2475
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1698
2476
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
@@ -1722,8 +2500,7 @@ function createIssuerService(config) {
|
|
|
1722
2500
|
if (config.fee) {
|
|
1723
2501
|
feeManager = new FeeManager({
|
|
1724
2502
|
...config.fee,
|
|
1725
|
-
provider: config.provider
|
|
1726
|
-
operatorWallet: config.operatorWallet
|
|
2503
|
+
provider: config.provider
|
|
1727
2504
|
});
|
|
1728
2505
|
}
|
|
1729
2506
|
const gatewayConfig = {
|
|
@@ -1799,6 +2576,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1799
2576
|
0 && (module.exports = {
|
|
1800
2577
|
AuthError,
|
|
1801
2578
|
AuthService,
|
|
2579
|
+
BalanceAggregator,
|
|
2580
|
+
BurnIndexer,
|
|
1802
2581
|
DefaultPolicyEngine,
|
|
1803
2582
|
FeeManager,
|
|
1804
2583
|
InMemoryCursorStore,
|
|
@@ -1809,10 +2588,16 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1809
2588
|
MintingGatewayError,
|
|
1810
2589
|
NonceManager,
|
|
1811
2590
|
PAFI_ISSUER_SDK_VERSION,
|
|
2591
|
+
PTRedeemError,
|
|
2592
|
+
PTRedeemHandler,
|
|
2593
|
+
PafiBackendClient,
|
|
2594
|
+
PafiBackendError,
|
|
1812
2595
|
PointIndexer,
|
|
1813
2596
|
PrivateKeySigner,
|
|
1814
2597
|
RelayError,
|
|
1815
2598
|
RelayService,
|
|
2599
|
+
TopUpRedemptionError,
|
|
2600
|
+
TopUpRedemptionHandler,
|
|
1816
2601
|
authenticateRequest,
|
|
1817
2602
|
createIssuerService,
|
|
1818
2603
|
createSubgraphNativeUsdtQuoter,
|