@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.js
CHANGED
|
@@ -116,6 +116,54 @@ var MemoryPointLedger = class {
|
|
|
116
116
|
if (txHash) lock.txHash = txHash;
|
|
117
117
|
}
|
|
118
118
|
// -------------------------------------------------------------------------
|
|
119
|
+
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
pendingCredits = /* @__PURE__ */ new Map();
|
|
122
|
+
nextCreditId = 1;
|
|
123
|
+
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
124
|
+
if (amount <= 0n) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"MemoryPointLedger: pending credit amount must be positive"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (durationMs <= 0) {
|
|
130
|
+
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
131
|
+
}
|
|
132
|
+
const user = getAddress(userAddress);
|
|
133
|
+
const lockId = `credit-${this.nextCreditId++}`;
|
|
134
|
+
const now = this.now();
|
|
135
|
+
this.pendingCredits.set(lockId, {
|
|
136
|
+
lockId,
|
|
137
|
+
userAddress: user,
|
|
138
|
+
amount,
|
|
139
|
+
tokenAddress: tokenAddress !== void 0 ? getAddress(tokenAddress) : void 0,
|
|
140
|
+
createdAt: now,
|
|
141
|
+
expiresAt: now + durationMs,
|
|
142
|
+
status: "PENDING"
|
|
143
|
+
});
|
|
144
|
+
return lockId;
|
|
145
|
+
}
|
|
146
|
+
async resolveCreditByBurnTx(lockId, txHash) {
|
|
147
|
+
const credit = this.pendingCredits.get(lockId);
|
|
148
|
+
if (!credit) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (credit.status === "RESOLVED") {
|
|
154
|
+
if (credit.txHash === txHash) return;
|
|
155
|
+
throw new Error(
|
|
156
|
+
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const token = normalizeToken(credit.tokenAddress);
|
|
160
|
+
const key = balanceKey(credit.userAddress, token);
|
|
161
|
+
const current = this.balances.get(key) ?? 0n;
|
|
162
|
+
this.balances.set(key, current + credit.amount);
|
|
163
|
+
credit.status = "RESOLVED";
|
|
164
|
+
credit.txHash = txHash;
|
|
165
|
+
}
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
119
167
|
// Internal helpers
|
|
120
168
|
// -------------------------------------------------------------------------
|
|
121
169
|
/**
|
|
@@ -534,11 +582,15 @@ var RelayError = class extends Error {
|
|
|
534
582
|
};
|
|
535
583
|
|
|
536
584
|
// src/relay/relayService.ts
|
|
585
|
+
import { encodeFunctionData } from "viem";
|
|
537
586
|
import {
|
|
538
587
|
relayAbi,
|
|
539
588
|
encodeMintAndSwap,
|
|
540
589
|
simulateMintAndSwap as coreSimulateMintAndSwap,
|
|
541
|
-
SimulationError
|
|
590
|
+
SimulationError,
|
|
591
|
+
RELAYER_V2_ABI,
|
|
592
|
+
POINT_TOKEN_V2_ABI,
|
|
593
|
+
buildPartialUserOperation
|
|
542
594
|
} from "@pafi-dev/core";
|
|
543
595
|
var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
544
596
|
var RelayService = class {
|
|
@@ -591,6 +643,11 @@ var RelayService = class {
|
|
|
591
643
|
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
592
644
|
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
593
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.
|
|
594
651
|
*/
|
|
595
652
|
async submitMintAndSwap(params) {
|
|
596
653
|
if (this.simulateBeforeSubmit && this.provider) {
|
|
@@ -659,6 +716,154 @@ var RelayService = class {
|
|
|
659
716
|
);
|
|
660
717
|
}
|
|
661
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");
|
|
748
|
+
}
|
|
749
|
+
if (!params.batchExecutorAddress) {
|
|
750
|
+
throw new RelayError(
|
|
751
|
+
"ENCODE_FAILED",
|
|
752
|
+
"prepareMint: batchExecutorAddress required"
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (!params.userAddress) {
|
|
756
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
|
|
757
|
+
}
|
|
758
|
+
let mintCallData;
|
|
759
|
+
try {
|
|
760
|
+
mintCallData = encodeFunctionData({
|
|
761
|
+
abi: RELAYER_V2_ABI,
|
|
762
|
+
functionName: "mint",
|
|
763
|
+
args: [params.mintRequest, params.userSignature, params.issuerSignature]
|
|
764
|
+
});
|
|
765
|
+
} catch (err) {
|
|
766
|
+
throw new RelayError(
|
|
767
|
+
"ENCODE_FAILED",
|
|
768
|
+
`prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
|
|
769
|
+
err
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const operations = [
|
|
773
|
+
{
|
|
774
|
+
target: params.relayerAddress,
|
|
775
|
+
value: 0n,
|
|
776
|
+
data: mintCallData
|
|
777
|
+
}
|
|
778
|
+
];
|
|
779
|
+
if (params.mintRequest.feeAmount > 0n) {
|
|
780
|
+
operations.push({
|
|
781
|
+
target: params.pointTokenAddress,
|
|
782
|
+
value: 0n,
|
|
783
|
+
data: encodeFunctionData({
|
|
784
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
785
|
+
functionName: "balanceOf",
|
|
786
|
+
// placeholder — real impl uses transfer
|
|
787
|
+
args: [params.mintRequest.feeRecipient]
|
|
788
|
+
})
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return buildPartialUserOperation({
|
|
792
|
+
sender: params.userAddress,
|
|
793
|
+
nonce: params.aaNonce,
|
|
794
|
+
operations,
|
|
795
|
+
gasLimits: {
|
|
796
|
+
callGasLimit: params.callGasLimit ?? 500000n,
|
|
797
|
+
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
798
|
+
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
|
|
804
|
+
*
|
|
805
|
+
* Two modes:
|
|
806
|
+
* - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
|
|
807
|
+
* via EIP-7702 delegation is the user, so no signature needed
|
|
808
|
+
* on-chain (the BurnConsent was already verified off-chain by
|
|
809
|
+
* the issuer backend before we got here)
|
|
810
|
+
* - `mode: 'burnWithSig'` — `PointToken.burnWithSig(consent, sig)`;
|
|
811
|
+
* used when the issuer hasn't verified the consent and the
|
|
812
|
+
* contract has to do it on-chain
|
|
813
|
+
*/
|
|
814
|
+
prepareBurn(params) {
|
|
815
|
+
if (!params.pointTokenAddress) {
|
|
816
|
+
throw new RelayError("ENCODE_FAILED", "prepareBurn: pointTokenAddress required");
|
|
817
|
+
}
|
|
818
|
+
if (!params.batchExecutorAddress) {
|
|
819
|
+
throw new RelayError(
|
|
820
|
+
"ENCODE_FAILED",
|
|
821
|
+
"prepareBurn: batchExecutorAddress required"
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
let burnCallData;
|
|
825
|
+
try {
|
|
826
|
+
if (params.mode === "burnWithSig") {
|
|
827
|
+
if (!params.burnConsent || !params.consentSignature) {
|
|
828
|
+
throw new Error("burnWithSig requires burnConsent + consentSignature");
|
|
829
|
+
}
|
|
830
|
+
burnCallData = encodeFunctionData({
|
|
831
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
832
|
+
functionName: "burnWithSig",
|
|
833
|
+
args: [params.burnConsent, params.consentSignature]
|
|
834
|
+
});
|
|
835
|
+
} else {
|
|
836
|
+
burnCallData = encodeFunctionData({
|
|
837
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
838
|
+
functionName: "burn",
|
|
839
|
+
args: [params.amount]
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
} catch (err) {
|
|
843
|
+
throw new RelayError(
|
|
844
|
+
"ENCODE_FAILED",
|
|
845
|
+
`prepareBurn: failed to encode burn call: ${errorMessage(err)}`,
|
|
846
|
+
err
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const operations = [
|
|
850
|
+
{
|
|
851
|
+
target: params.pointTokenAddress,
|
|
852
|
+
value: 0n,
|
|
853
|
+
data: burnCallData
|
|
854
|
+
}
|
|
855
|
+
];
|
|
856
|
+
return buildPartialUserOperation({
|
|
857
|
+
sender: params.userAddress,
|
|
858
|
+
nonce: params.aaNonce,
|
|
859
|
+
operations,
|
|
860
|
+
gasLimits: {
|
|
861
|
+
callGasLimit: params.callGasLimit ?? 300000n,
|
|
862
|
+
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
863
|
+
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
662
867
|
};
|
|
663
868
|
function errorMessage(err) {
|
|
664
869
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -669,84 +874,35 @@ var DEFAULT_GAS_UNITS = 500000n;
|
|
|
669
874
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
670
875
|
var FeeManager = class {
|
|
671
876
|
provider;
|
|
672
|
-
|
|
673
|
-
mintAndSwapGasUnits;
|
|
877
|
+
gasUnits;
|
|
674
878
|
gasPremiumBps;
|
|
675
|
-
|
|
676
|
-
rebalanceThresholdWei;
|
|
677
|
-
rebalanceUsdtAmount;
|
|
678
|
-
swapUsdtToNative;
|
|
879
|
+
quoteNativeToFee;
|
|
679
880
|
constructor(config) {
|
|
680
881
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
681
|
-
if (!config.
|
|
682
|
-
throw new Error("FeeManager:
|
|
683
|
-
if (!config.quoteNativeToUsdt)
|
|
684
|
-
throw new Error("FeeManager: quoteNativeToUsdt required");
|
|
882
|
+
if (!config.quoteNativeToFee)
|
|
883
|
+
throw new Error("FeeManager: quoteNativeToFee required");
|
|
685
884
|
this.provider = config.provider;
|
|
686
|
-
this.
|
|
687
|
-
this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
|
|
885
|
+
this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
|
|
688
886
|
this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
|
|
689
|
-
this.
|
|
690
|
-
if (config.rebalanceThresholdWei !== void 0) {
|
|
691
|
-
this.rebalanceThresholdWei = config.rebalanceThresholdWei;
|
|
692
|
-
}
|
|
693
|
-
if (config.rebalanceUsdtAmount !== void 0) {
|
|
694
|
-
this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
|
|
695
|
-
}
|
|
696
|
-
if (config.swapUsdtToNative) {
|
|
697
|
-
this.swapUsdtToNative = config.swapUsdtToNative;
|
|
698
|
-
}
|
|
699
|
-
const rebalanceFields = [
|
|
700
|
-
config.rebalanceThresholdWei,
|
|
701
|
-
config.rebalanceUsdtAmount,
|
|
702
|
-
config.swapUsdtToNative
|
|
703
|
-
];
|
|
704
|
-
const someSet = rebalanceFields.some((v) => v !== void 0);
|
|
705
|
-
const allSet = rebalanceFields.every((v) => v !== void 0);
|
|
706
|
-
if (someSet && !allSet) {
|
|
707
|
-
throw new Error(
|
|
708
|
-
"FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
|
|
709
|
-
);
|
|
710
|
-
}
|
|
887
|
+
this.quoteNativeToFee = config.quoteNativeToFee;
|
|
711
888
|
}
|
|
712
889
|
/**
|
|
713
|
-
* Estimate the
|
|
714
|
-
*
|
|
890
|
+
* Estimate the fee (in the caller's fee currency) to charge for the
|
|
891
|
+
* next sponsored UserOp:
|
|
892
|
+
*
|
|
893
|
+
* nativeCost = gasUnits × gasPrice
|
|
894
|
+
* withPremium = nativeCost × premiumBps / 10_000
|
|
895
|
+
* fee = quoteNativeToFee(withPremium)
|
|
715
896
|
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
897
|
+
* For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
|
|
898
|
+
* from the response, the name `estimateGasFee` is kept — but the
|
|
899
|
+
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
719
900
|
*/
|
|
720
901
|
async estimateGasFee() {
|
|
721
902
|
const gasPrice = await this.provider.getGasPrice();
|
|
722
|
-
const nativeCost = gasPrice * this.
|
|
903
|
+
const nativeCost = gasPrice * this.gasUnits;
|
|
723
904
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
724
|
-
return this.
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Check the operator's native balance and, if it has dropped below the
|
|
728
|
-
* configured threshold, trigger a USDT→native rebalance via the injected
|
|
729
|
-
* `swapUsdtToNative` function.
|
|
730
|
-
*
|
|
731
|
-
* Returns `true` if a rebalance was performed, `false` otherwise.
|
|
732
|
-
* Silently no-ops when rebalance is not configured.
|
|
733
|
-
*/
|
|
734
|
-
async rebalanceIfNeeded() {
|
|
735
|
-
if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
|
|
736
|
-
return false;
|
|
737
|
-
}
|
|
738
|
-
const operatorAddress = this.operatorWallet.account?.address;
|
|
739
|
-
if (!operatorAddress) {
|
|
740
|
-
throw new Error(
|
|
741
|
-
"FeeManager: operator wallet has no account attached \u2014 cannot read balance"
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
const balance = await this.provider.getBalance({ address: operatorAddress });
|
|
745
|
-
if (balance >= this.rebalanceThresholdWei) {
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
await this.swapUsdtToNative(this.rebalanceUsdtAmount);
|
|
749
|
-
return true;
|
|
905
|
+
return this.quoteNativeToFee(withPremium);
|
|
750
906
|
}
|
|
751
907
|
};
|
|
752
908
|
|
|
@@ -795,6 +951,12 @@ var MintingGateway = class {
|
|
|
795
951
|
this.now = config.now ?? (() => Date.now());
|
|
796
952
|
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
797
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
|
+
*/
|
|
798
960
|
async processMintAndCashOut(request) {
|
|
799
961
|
const { receiverConsent, receiverSignature } = request;
|
|
800
962
|
if (!receiverConsent || !receiverSignature) {
|
|
@@ -1184,8 +1346,159 @@ function pickMatchingLock(locks, amount) {
|
|
|
1184
1346
|
return best;
|
|
1185
1347
|
}
|
|
1186
1348
|
|
|
1349
|
+
// src/indexer/burnIndexer.ts
|
|
1350
|
+
import { getAddress as getAddress5, parseAbiItem as parseAbiItem2 } from "viem";
|
|
1351
|
+
var TRANSFER_EVENT2 = parseAbiItem2(
|
|
1352
|
+
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1353
|
+
);
|
|
1354
|
+
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
1355
|
+
var DEFAULT_CONFIRMATIONS2 = 3;
|
|
1356
|
+
var DEFAULT_BATCH_SIZE2 = 2000n;
|
|
1357
|
+
var DEFAULT_POLL_INTERVAL_MS2 = 5e3;
|
|
1358
|
+
var BurnIndexer = class {
|
|
1359
|
+
provider;
|
|
1360
|
+
pointTokenAddress;
|
|
1361
|
+
ledger;
|
|
1362
|
+
cursorStore;
|
|
1363
|
+
startBlock;
|
|
1364
|
+
confirmations;
|
|
1365
|
+
batchSize;
|
|
1366
|
+
pollIntervalMs;
|
|
1367
|
+
/**
|
|
1368
|
+
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1369
|
+
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1370
|
+
* ledger's query path.
|
|
1371
|
+
*
|
|
1372
|
+
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1373
|
+
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1374
|
+
* incrementing IDs so callers with the memory ledger must provide a
|
|
1375
|
+
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1376
|
+
* their `pending_credits` table.
|
|
1377
|
+
*/
|
|
1378
|
+
matchLockId = async () => void 0;
|
|
1379
|
+
running = false;
|
|
1380
|
+
timer;
|
|
1381
|
+
constructor(config) {
|
|
1382
|
+
if (!config.provider) throw new Error("BurnIndexer: provider required");
|
|
1383
|
+
if (!config.pointTokenAddress)
|
|
1384
|
+
throw new Error("BurnIndexer: pointTokenAddress required");
|
|
1385
|
+
if (!config.ledger) throw new Error("BurnIndexer: ledger required");
|
|
1386
|
+
this.provider = config.provider;
|
|
1387
|
+
this.pointTokenAddress = config.pointTokenAddress;
|
|
1388
|
+
this.ledger = config.ledger;
|
|
1389
|
+
this.cursorStore = config.cursorStore ?? new InMemoryCursorStore();
|
|
1390
|
+
this.startBlock = config.fromBlock ?? 0n;
|
|
1391
|
+
this.confirmations = BigInt(
|
|
1392
|
+
config.confirmations ?? DEFAULT_CONFIRMATIONS2
|
|
1393
|
+
);
|
|
1394
|
+
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1395
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
1396
|
+
}
|
|
1397
|
+
start() {
|
|
1398
|
+
if (this.running) return;
|
|
1399
|
+
this.running = true;
|
|
1400
|
+
void this.tick();
|
|
1401
|
+
}
|
|
1402
|
+
stop() {
|
|
1403
|
+
this.running = false;
|
|
1404
|
+
if (this.timer) {
|
|
1405
|
+
clearTimeout(this.timer);
|
|
1406
|
+
this.timer = void 0;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async tick() {
|
|
1410
|
+
if (!this.running) return;
|
|
1411
|
+
try {
|
|
1412
|
+
const latest = await this.provider.getBlockNumber();
|
|
1413
|
+
const safeHead = latest - this.confirmations;
|
|
1414
|
+
if (safeHead < 0n) {
|
|
1415
|
+
this.scheduleNext();
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const stored = await this.cursorStore.load();
|
|
1419
|
+
const from = stored ?? this.startBlock;
|
|
1420
|
+
if (from > safeHead) {
|
|
1421
|
+
this.scheduleNext();
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
await this.processBlockRange(from, safeHead);
|
|
1425
|
+
} catch {
|
|
1426
|
+
}
|
|
1427
|
+
this.scheduleNext();
|
|
1428
|
+
}
|
|
1429
|
+
scheduleNext() {
|
|
1430
|
+
if (!this.running) return;
|
|
1431
|
+
this.timer = setTimeout(() => void this.tick(), this.pollIntervalMs);
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Scan `[from, to]` inclusive for burn events. Callers can drive this
|
|
1435
|
+
* directly to backfill a specific range without `start()`. Cursor is
|
|
1436
|
+
* advanced to `to + 1` on completion.
|
|
1437
|
+
*/
|
|
1438
|
+
async processBlockRange(from, to) {
|
|
1439
|
+
if (from > to) return;
|
|
1440
|
+
let cursor = from;
|
|
1441
|
+
while (cursor <= to) {
|
|
1442
|
+
const chunkEnd = cursor + this.batchSize - 1n > to ? to : cursor + this.batchSize - 1n;
|
|
1443
|
+
const logs = await this.provider.getLogs({
|
|
1444
|
+
address: this.pointTokenAddress,
|
|
1445
|
+
event: TRANSFER_EVENT2,
|
|
1446
|
+
args: { to: ZERO_ADDRESS2 },
|
|
1447
|
+
// filter: burn = transfer to zero
|
|
1448
|
+
fromBlock: cursor,
|
|
1449
|
+
toBlock: chunkEnd
|
|
1450
|
+
});
|
|
1451
|
+
const events = this.decodeBurnEvents(logs);
|
|
1452
|
+
events.sort((a, b) => {
|
|
1453
|
+
if (a.blockNumber !== b.blockNumber) {
|
|
1454
|
+
return a.blockNumber < b.blockNumber ? -1 : 1;
|
|
1455
|
+
}
|
|
1456
|
+
return a.logIndex - b.logIndex;
|
|
1457
|
+
});
|
|
1458
|
+
for (const evt of events) {
|
|
1459
|
+
await this.finalize(evt);
|
|
1460
|
+
}
|
|
1461
|
+
await this.cursorStore.save(chunkEnd + 1n);
|
|
1462
|
+
cursor = chunkEnd + 1n;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
decodeBurnEvents(logs) {
|
|
1466
|
+
const out = [];
|
|
1467
|
+
for (const log of logs) {
|
|
1468
|
+
const args = log.args;
|
|
1469
|
+
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1470
|
+
if (getAddress5(args.to) !== ZERO_ADDRESS2) continue;
|
|
1471
|
+
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1472
|
+
out.push({
|
|
1473
|
+
from: getAddress5(args.from),
|
|
1474
|
+
amount: args.value,
|
|
1475
|
+
blockNumber: log.blockNumber,
|
|
1476
|
+
txHash: log.transactionHash,
|
|
1477
|
+
logIndex: log.logIndex ?? 0
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return out;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Resolve a matching pending credit for this burn event and call
|
|
1484
|
+
* `ledger.resolveCreditByBurnTx(lockId, txHash)`. If no match found,
|
|
1485
|
+
* log + skip.
|
|
1486
|
+
*/
|
|
1487
|
+
async finalize(evt) {
|
|
1488
|
+
const lockId = await this.matchLockId(evt);
|
|
1489
|
+
if (!lockId) return;
|
|
1490
|
+
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1495
|
+
} catch {
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1187
1500
|
// src/api/handlers.ts
|
|
1188
|
-
import { getAddress as
|
|
1501
|
+
import { getAddress as getAddress6 } from "viem";
|
|
1189
1502
|
import {
|
|
1190
1503
|
getMintRequestNonce,
|
|
1191
1504
|
getPointTokenBalance,
|
|
@@ -1210,6 +1523,7 @@ var IssuerApiHandlers = class {
|
|
|
1210
1523
|
defaultToken;
|
|
1211
1524
|
chainId;
|
|
1212
1525
|
contracts;
|
|
1526
|
+
pafiWebUrl;
|
|
1213
1527
|
feeManager;
|
|
1214
1528
|
poolsProvider;
|
|
1215
1529
|
constructor(config) {
|
|
@@ -1223,11 +1537,12 @@ var IssuerApiHandlers = class {
|
|
|
1223
1537
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1224
1538
|
);
|
|
1225
1539
|
}
|
|
1226
|
-
const normalized = raw.map((a) =>
|
|
1540
|
+
const normalized = raw.map((a) => getAddress6(a));
|
|
1227
1541
|
this.supportedTokens = new Set(normalized);
|
|
1228
1542
|
this.defaultToken = normalized[0];
|
|
1229
1543
|
this.chainId = config.chainId;
|
|
1230
1544
|
this.contracts = config.contracts;
|
|
1545
|
+
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1231
1546
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1232
1547
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1233
1548
|
}
|
|
@@ -1263,7 +1578,16 @@ var IssuerApiHandlers = class {
|
|
|
1263
1578
|
`handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
|
|
1264
1579
|
);
|
|
1265
1580
|
}
|
|
1266
|
-
|
|
1581
|
+
const contracts = {
|
|
1582
|
+
...this.contracts,
|
|
1583
|
+
pointTokens: Array.from(this.supportedTokens)
|
|
1584
|
+
};
|
|
1585
|
+
const response = {
|
|
1586
|
+
chainId: this.chainId,
|
|
1587
|
+
contracts
|
|
1588
|
+
};
|
|
1589
|
+
if (this.pafiWebUrl) response.pafiWebUrl = this.pafiWebUrl;
|
|
1590
|
+
return response;
|
|
1267
1591
|
}
|
|
1268
1592
|
/** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
|
|
1269
1593
|
async handleGasFee() {
|
|
@@ -1314,14 +1638,14 @@ var IssuerApiHandlers = class {
|
|
|
1314
1638
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1315
1639
|
);
|
|
1316
1640
|
}
|
|
1317
|
-
const normalizedAuthed =
|
|
1318
|
-
const normalizedRequest =
|
|
1641
|
+
const normalizedAuthed = getAddress6(userAddress);
|
|
1642
|
+
const normalizedRequest = getAddress6(request.userAddress);
|
|
1319
1643
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1320
1644
|
throw new Error(
|
|
1321
1645
|
"handleUser: request userAddress must match authenticated user"
|
|
1322
1646
|
);
|
|
1323
1647
|
}
|
|
1324
|
-
const pointToken =
|
|
1648
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1325
1649
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1326
1650
|
throw new Error(
|
|
1327
1651
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
@@ -1364,7 +1688,7 @@ var IssuerApiHandlers = class {
|
|
|
1364
1688
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1365
1689
|
);
|
|
1366
1690
|
}
|
|
1367
|
-
const pointToken =
|
|
1691
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1368
1692
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1369
1693
|
throw new Error(
|
|
1370
1694
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
@@ -1389,8 +1713,14 @@ var IssuerApiHandlers = class {
|
|
|
1389
1713
|
/**
|
|
1390
1714
|
* `POST /claim-and-swap`
|
|
1391
1715
|
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
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.
|
|
1394
1724
|
*/
|
|
1395
1725
|
async handleClaimAndSwap(userAddress, request) {
|
|
1396
1726
|
if (request.chainId !== this.chainId) {
|
|
@@ -1398,14 +1728,14 @@ var IssuerApiHandlers = class {
|
|
|
1398
1728
|
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1399
1729
|
);
|
|
1400
1730
|
}
|
|
1401
|
-
const pointToken =
|
|
1731
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1402
1732
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1403
1733
|
throw new Error(
|
|
1404
1734
|
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1405
1735
|
);
|
|
1406
1736
|
}
|
|
1407
1737
|
const result = await this.gateway.processMintAndCashOut({
|
|
1408
|
-
userAddress:
|
|
1738
|
+
userAddress: getAddress6(userAddress),
|
|
1409
1739
|
pointTokenAddress: pointToken,
|
|
1410
1740
|
chainId: request.chainId,
|
|
1411
1741
|
domain: request.domain,
|
|
@@ -1425,6 +1755,183 @@ var IssuerApiHandlers = class {
|
|
|
1425
1755
|
}
|
|
1426
1756
|
};
|
|
1427
1757
|
|
|
1758
|
+
// src/api/handlers/ptRedeemHandler.ts
|
|
1759
|
+
import { getAddress as getAddress7 } from "viem";
|
|
1760
|
+
import { verifyBurnConsent } from "@pafi-dev/core";
|
|
1761
|
+
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1762
|
+
var PTRedeemError = class extends Error {
|
|
1763
|
+
constructor(code, message) {
|
|
1764
|
+
super(message);
|
|
1765
|
+
this.code = code;
|
|
1766
|
+
this.name = "PTRedeemError";
|
|
1767
|
+
}
|
|
1768
|
+
code;
|
|
1769
|
+
};
|
|
1770
|
+
var PTRedeemHandler = class {
|
|
1771
|
+
ledger;
|
|
1772
|
+
relayService;
|
|
1773
|
+
pointTokenAddress;
|
|
1774
|
+
batchExecutorAddress;
|
|
1775
|
+
chainId;
|
|
1776
|
+
domain;
|
|
1777
|
+
redeemLockDurationMs;
|
|
1778
|
+
now;
|
|
1779
|
+
constructor(config) {
|
|
1780
|
+
if (!config.ledger.reservePendingCredit) {
|
|
1781
|
+
throw new PTRedeemError(
|
|
1782
|
+
"LEDGER_NOT_SUPPORTED",
|
|
1783
|
+
"PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
this.ledger = config.ledger;
|
|
1787
|
+
this.relayService = config.relayService;
|
|
1788
|
+
this.pointTokenAddress = getAddress7(config.pointTokenAddress);
|
|
1789
|
+
this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
|
|
1790
|
+
this.chainId = config.chainId;
|
|
1791
|
+
this.domain = config.domain;
|
|
1792
|
+
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1793
|
+
this.now = config.now ?? (() => Date.now());
|
|
1794
|
+
}
|
|
1795
|
+
async handle(request) {
|
|
1796
|
+
if (request.amount <= 0n) {
|
|
1797
|
+
throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
|
|
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
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
|
|
1806
|
+
if (request.consent.deadline <= nowSeconds) {
|
|
1807
|
+
throw new PTRedeemError(
|
|
1808
|
+
"EXPIRED_CONSENT",
|
|
1809
|
+
`consent deadline (${request.consent.deadline}) already passed`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
const verification = await verifyBurnConsent(
|
|
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
|
|
1821
|
+
);
|
|
1822
|
+
if (!verification.isValid) {
|
|
1823
|
+
throw new PTRedeemError(
|
|
1824
|
+
"SIGNATURE_MISMATCH",
|
|
1825
|
+
`signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const lockId = await this.ledger.reservePendingCredit(
|
|
1829
|
+
request.userAddress,
|
|
1830
|
+
request.amount,
|
|
1831
|
+
this.redeemLockDurationMs,
|
|
1832
|
+
this.pointTokenAddress
|
|
1833
|
+
);
|
|
1834
|
+
const userOp = this.relayService.prepareBurn({
|
|
1835
|
+
mode: "burnWithSig",
|
|
1836
|
+
userAddress: request.userAddress,
|
|
1837
|
+
aaNonce: request.aaNonce,
|
|
1838
|
+
pointTokenAddress: this.pointTokenAddress,
|
|
1839
|
+
batchExecutorAddress: this.batchExecutorAddress,
|
|
1840
|
+
burnConsent: request.consent,
|
|
1841
|
+
consentSignature: parseSigStruct(request.consentSignature)
|
|
1842
|
+
});
|
|
1843
|
+
return {
|
|
1844
|
+
lockId,
|
|
1845
|
+
userOp,
|
|
1846
|
+
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
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
|
+
|
|
1864
|
+
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1865
|
+
import { getAddress as getAddress8 } from "viem";
|
|
1866
|
+
import { getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
|
|
1867
|
+
var TopUpRedemptionError = class extends Error {
|
|
1868
|
+
constructor(code, message) {
|
|
1869
|
+
super(message);
|
|
1870
|
+
this.code = code;
|
|
1871
|
+
this.name = "TopUpRedemptionError";
|
|
1872
|
+
}
|
|
1873
|
+
code;
|
|
1874
|
+
};
|
|
1875
|
+
var TopUpRedemptionHandler = class {
|
|
1876
|
+
ledger;
|
|
1877
|
+
ptRedeemHandler;
|
|
1878
|
+
provider;
|
|
1879
|
+
pointTokenAddress;
|
|
1880
|
+
constructor(config) {
|
|
1881
|
+
this.ledger = config.ledger;
|
|
1882
|
+
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1883
|
+
this.provider = config.provider;
|
|
1884
|
+
this.pointTokenAddress = getAddress8(config.pointTokenAddress);
|
|
1885
|
+
}
|
|
1886
|
+
async handle(request) {
|
|
1887
|
+
const offChainBalance = await this.ledger.getBalance(
|
|
1888
|
+
request.userAddress,
|
|
1889
|
+
this.pointTokenAddress
|
|
1890
|
+
);
|
|
1891
|
+
if (offChainBalance >= request.requiredAmount) {
|
|
1892
|
+
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1893
|
+
}
|
|
1894
|
+
const shortfall = request.requiredAmount - offChainBalance;
|
|
1895
|
+
const onChainBalance = await getPointTokenBalance2(
|
|
1896
|
+
this.provider,
|
|
1897
|
+
this.pointTokenAddress,
|
|
1898
|
+
request.userAddress
|
|
1899
|
+
);
|
|
1900
|
+
if (onChainBalance < shortfall) {
|
|
1901
|
+
return {
|
|
1902
|
+
action: "INSUFFICIENT_ONCHAIN",
|
|
1903
|
+
offChainBalance,
|
|
1904
|
+
onChainBalance,
|
|
1905
|
+
shortfall
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
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
|
+
const redeem = await this.ptRedeemHandler.handle({
|
|
1921
|
+
userAddress: request.userAddress,
|
|
1922
|
+
amount: shortfall,
|
|
1923
|
+
consent: request.redeemRequest.consent,
|
|
1924
|
+
consentSignature: request.redeemRequest.consentSignature,
|
|
1925
|
+
aaNonce: request.redeemRequest.aaNonce
|
|
1926
|
+
});
|
|
1927
|
+
return {
|
|
1928
|
+
action: "TOP_UP_STARTED",
|
|
1929
|
+
shortfall,
|
|
1930
|
+
redeem
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1428
1935
|
// src/pools/subgraphPoolsProvider.ts
|
|
1429
1936
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1430
1937
|
var POOL_QUERY = `
|
|
@@ -1636,8 +2143,274 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1636
2143
|
return BigInt(whole + padded);
|
|
1637
2144
|
}
|
|
1638
2145
|
|
|
2146
|
+
// src/balance/balanceAggregator.ts
|
|
2147
|
+
import { getPointTokenBalance as getPointTokenBalance3 } from "@pafi-dev/core";
|
|
2148
|
+
var BalanceAggregator = class {
|
|
2149
|
+
provider;
|
|
2150
|
+
ledger;
|
|
2151
|
+
constructor(config) {
|
|
2152
|
+
if (!config.provider) {
|
|
2153
|
+
throw new Error("BalanceAggregator: provider is required");
|
|
2154
|
+
}
|
|
2155
|
+
if (!config.ledger) {
|
|
2156
|
+
throw new Error("BalanceAggregator: ledger is required");
|
|
2157
|
+
}
|
|
2158
|
+
this.provider = config.provider;
|
|
2159
|
+
this.ledger = config.ledger;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Combined balance for a single (user, token) pair. Fetches off-chain
|
|
2163
|
+
* + on-chain in parallel.
|
|
2164
|
+
*/
|
|
2165
|
+
async getCombinedBalance(user, pointToken) {
|
|
2166
|
+
const [offChain, onChain] = await Promise.all([
|
|
2167
|
+
this.ledger.getBalance(user, pointToken),
|
|
2168
|
+
getPointTokenBalance3(this.provider, pointToken, user)
|
|
2169
|
+
]);
|
|
2170
|
+
return {
|
|
2171
|
+
offChain,
|
|
2172
|
+
onChain,
|
|
2173
|
+
total: offChain + onChain
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Combined balance for multiple tokens owned by the same user. Runs
|
|
2178
|
+
* all lookups in parallel. Returns a Map keyed by the token address
|
|
2179
|
+
* (same casing as supplied — caller should normalize if needed).
|
|
2180
|
+
*/
|
|
2181
|
+
async getCombinedBalanceMulti(user, pointTokens) {
|
|
2182
|
+
const entries = await Promise.all(
|
|
2183
|
+
pointTokens.map(async (token) => {
|
|
2184
|
+
const balance = await this.getCombinedBalance(user, token);
|
|
2185
|
+
return [token, balance];
|
|
2186
|
+
})
|
|
2187
|
+
);
|
|
2188
|
+
return new Map(entries);
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
|
|
2192
|
+
// src/pafi-backend/types.ts
|
|
2193
|
+
var PafiBackendError = class extends Error {
|
|
2194
|
+
constructor(code, message, httpStatus, details, opts) {
|
|
2195
|
+
super(message);
|
|
2196
|
+
this.code = code;
|
|
2197
|
+
this.httpStatus = httpStatus;
|
|
2198
|
+
this.details = details;
|
|
2199
|
+
this.name = "PafiBackendError";
|
|
2200
|
+
if (opts?.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
|
|
2201
|
+
if (opts?.safeToRetry !== void 0) this.serverSafeToRetry = opts.safeToRetry;
|
|
2202
|
+
}
|
|
2203
|
+
code;
|
|
2204
|
+
httpStatus;
|
|
2205
|
+
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
|
+
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
|
+
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
|
+
get safeToRetry() {
|
|
2224
|
+
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
2225
|
+
switch (this.code) {
|
|
2226
|
+
case "PAYMASTER_UNAVAILABLE":
|
|
2227
|
+
case "PAYMASTER_TIMEOUT":
|
|
2228
|
+
case "RATE_LIMITER_UNAVAILABLE":
|
|
2229
|
+
case "INTERNAL_ERROR":
|
|
2230
|
+
case "TIMEOUT":
|
|
2231
|
+
case "NETWORK_ERROR":
|
|
2232
|
+
return true;
|
|
2233
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
2234
|
+
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
2235
|
+
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
2236
|
+
return true;
|
|
2237
|
+
// after retryAfter
|
|
2238
|
+
default:
|
|
2239
|
+
return false;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
|
|
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
|
+
|
|
1639
2412
|
// src/config.ts
|
|
1640
|
-
import { getAddress as
|
|
2413
|
+
import { getAddress as getAddress9 } from "viem";
|
|
1641
2414
|
function createIssuerService(config) {
|
|
1642
2415
|
if (!config.provider) {
|
|
1643
2416
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -1663,7 +2436,7 @@ function createIssuerService(config) {
|
|
|
1663
2436
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
1664
2437
|
);
|
|
1665
2438
|
}
|
|
1666
|
-
const tokenAddresses = rawAddresses.map((a) =>
|
|
2439
|
+
const tokenAddresses = rawAddresses.map((a) => getAddress9(a));
|
|
1667
2440
|
const ledger = config.ledger ?? new MemoryPointLedger();
|
|
1668
2441
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1669
2442
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
@@ -1693,8 +2466,7 @@ function createIssuerService(config) {
|
|
|
1693
2466
|
if (config.fee) {
|
|
1694
2467
|
feeManager = new FeeManager({
|
|
1695
2468
|
...config.fee,
|
|
1696
|
-
provider: config.provider
|
|
1697
|
-
operatorWallet: config.operatorWallet
|
|
2469
|
+
provider: config.provider
|
|
1698
2470
|
});
|
|
1699
2471
|
}
|
|
1700
2472
|
const gatewayConfig = {
|
|
@@ -1769,6 +2541,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1769
2541
|
export {
|
|
1770
2542
|
AuthError,
|
|
1771
2543
|
AuthService,
|
|
2544
|
+
BalanceAggregator,
|
|
2545
|
+
BurnIndexer,
|
|
1772
2546
|
DefaultPolicyEngine,
|
|
1773
2547
|
FeeManager,
|
|
1774
2548
|
InMemoryCursorStore,
|
|
@@ -1779,10 +2553,16 @@ export {
|
|
|
1779
2553
|
MintingGatewayError,
|
|
1780
2554
|
NonceManager,
|
|
1781
2555
|
PAFI_ISSUER_SDK_VERSION,
|
|
2556
|
+
PTRedeemError,
|
|
2557
|
+
PTRedeemHandler,
|
|
2558
|
+
PafiBackendClient,
|
|
2559
|
+
PafiBackendError,
|
|
1782
2560
|
PointIndexer,
|
|
1783
2561
|
PrivateKeySigner,
|
|
1784
2562
|
RelayError,
|
|
1785
2563
|
RelayService,
|
|
2564
|
+
TopUpRedemptionError,
|
|
2565
|
+
TopUpRedemptionHandler,
|
|
1786
2566
|
authenticateRequest,
|
|
1787
2567
|
createIssuerService,
|
|
1788
2568
|
createSubgraphNativeUsdtQuoter,
|