@pafi-dev/issuer 0.1.2 → 0.3.0-alpha.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 +644 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +491 -94
- package/dist/index.d.ts +491 -94
- package/dist/index.js +640 -135
- 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,6 +34,8 @@ __export(index_exports, {
|
|
|
32
34
|
MintingGatewayError: () => MintingGatewayError,
|
|
33
35
|
NonceManager: () => NonceManager,
|
|
34
36
|
PAFI_ISSUER_SDK_VERSION: () => PAFI_ISSUER_SDK_VERSION,
|
|
37
|
+
PafiBackendClient: () => PafiBackendClient,
|
|
38
|
+
PafiBackendError: () => PafiBackendError,
|
|
35
39
|
PointIndexer: () => PointIndexer,
|
|
36
40
|
PrivateKeySigner: () => PrivateKeySigner,
|
|
37
41
|
RelayError: () => RelayError,
|
|
@@ -57,19 +61,21 @@ var MemoryPointLedger = class {
|
|
|
57
61
|
// -------------------------------------------------------------------------
|
|
58
62
|
// Read
|
|
59
63
|
// -------------------------------------------------------------------------
|
|
60
|
-
async getBalance(userAddress) {
|
|
61
|
-
const
|
|
64
|
+
async getBalance(userAddress, tokenAddress) {
|
|
65
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
66
|
+
const token = normalizeToken(tokenAddress);
|
|
62
67
|
this.purgeExpired();
|
|
63
|
-
const total = this.balances.get(
|
|
64
|
-
const locked = this.lockedTotalFor(
|
|
68
|
+
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
69
|
+
const locked = this.lockedTotalFor(user, token);
|
|
65
70
|
return total - locked;
|
|
66
71
|
}
|
|
67
|
-
async getLockedRequests(userAddress) {
|
|
68
|
-
const
|
|
72
|
+
async getLockedRequests(userAddress, tokenAddress) {
|
|
73
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
74
|
+
const token = normalizeToken(tokenAddress);
|
|
69
75
|
this.purgeExpired();
|
|
70
76
|
const out = [];
|
|
71
77
|
for (const lock of this.locks.values()) {
|
|
72
|
-
if (lock.userAddress ===
|
|
78
|
+
if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
73
79
|
out.push({ ...lock });
|
|
74
80
|
}
|
|
75
81
|
}
|
|
@@ -78,25 +84,28 @@ var MemoryPointLedger = class {
|
|
|
78
84
|
// -------------------------------------------------------------------------
|
|
79
85
|
// Write
|
|
80
86
|
// -------------------------------------------------------------------------
|
|
81
|
-
async creditBalance(userAddress, amount, _reason) {
|
|
87
|
+
async creditBalance(userAddress, amount, _reason, tokenAddress) {
|
|
82
88
|
if (amount <= 0n) {
|
|
83
89
|
throw new Error("MemoryPointLedger: credit amount must be positive");
|
|
84
90
|
}
|
|
85
|
-
const
|
|
91
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
92
|
+
const token = normalizeToken(tokenAddress);
|
|
93
|
+
const key = balanceKey(user, token);
|
|
86
94
|
const current = this.balances.get(key) ?? 0n;
|
|
87
95
|
this.balances.set(key, current + amount);
|
|
88
96
|
}
|
|
89
|
-
async lockForMinting(userAddress, amount, lockDurationMs) {
|
|
97
|
+
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
90
98
|
if (amount <= 0n) {
|
|
91
99
|
throw new Error("MemoryPointLedger: lock amount must be positive");
|
|
92
100
|
}
|
|
93
101
|
if (lockDurationMs <= 0) {
|
|
94
102
|
throw new Error("MemoryPointLedger: lockDurationMs must be positive");
|
|
95
103
|
}
|
|
96
|
-
const
|
|
104
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
105
|
+
const token = normalizeToken(tokenAddress);
|
|
97
106
|
this.purgeExpired();
|
|
98
|
-
const total = this.balances.get(
|
|
99
|
-
const alreadyLocked = this.lockedTotalFor(
|
|
107
|
+
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
108
|
+
const alreadyLocked = this.lockedTotalFor(user, token);
|
|
100
109
|
const available = total - alreadyLocked;
|
|
101
110
|
if (available < amount) {
|
|
102
111
|
throw new Error(
|
|
@@ -105,14 +114,18 @@ var MemoryPointLedger = class {
|
|
|
105
114
|
}
|
|
106
115
|
const lockId = `lock-${this.nextLockId++}`;
|
|
107
116
|
const now = this.now();
|
|
108
|
-
|
|
117
|
+
const lock = {
|
|
109
118
|
lockId,
|
|
110
|
-
userAddress:
|
|
119
|
+
userAddress: user,
|
|
111
120
|
amount,
|
|
112
121
|
status: "PENDING",
|
|
113
122
|
createdAt: now,
|
|
114
123
|
expiresAt: now + lockDurationMs
|
|
115
|
-
}
|
|
124
|
+
};
|
|
125
|
+
if (tokenAddress !== void 0) {
|
|
126
|
+
lock.tokenAddress = (0, import_viem.getAddress)(tokenAddress);
|
|
127
|
+
}
|
|
128
|
+
this.locks.set(lockId, lock);
|
|
116
129
|
return lockId;
|
|
117
130
|
}
|
|
118
131
|
async releaseLock(lockId) {
|
|
@@ -122,11 +135,13 @@ var MemoryPointLedger = class {
|
|
|
122
135
|
this.locks.delete(lockId);
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
|
-
async deductBalance(userAddress, amount, txHash) {
|
|
138
|
+
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
126
139
|
if (amount <= 0n) {
|
|
127
140
|
throw new Error("MemoryPointLedger: deduct amount must be positive");
|
|
128
141
|
}
|
|
129
|
-
const
|
|
142
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
143
|
+
const token = normalizeToken(tokenAddress);
|
|
144
|
+
const key = balanceKey(user, token);
|
|
130
145
|
const current = this.balances.get(key) ?? 0n;
|
|
131
146
|
if (current < amount) {
|
|
132
147
|
throw new Error(
|
|
@@ -135,7 +150,7 @@ var MemoryPointLedger = class {
|
|
|
135
150
|
}
|
|
136
151
|
this.balances.set(key, current - amount);
|
|
137
152
|
for (const lock of this.locks.values()) {
|
|
138
|
-
if (lock.userAddress ===
|
|
153
|
+
if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
139
154
|
lock.status = "MINTED";
|
|
140
155
|
lock.txHash = txHash;
|
|
141
156
|
return;
|
|
@@ -151,6 +166,54 @@ var MemoryPointLedger = class {
|
|
|
151
166
|
if (txHash) lock.txHash = txHash;
|
|
152
167
|
}
|
|
153
168
|
// -------------------------------------------------------------------------
|
|
169
|
+
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
170
|
+
// -------------------------------------------------------------------------
|
|
171
|
+
pendingCredits = /* @__PURE__ */ new Map();
|
|
172
|
+
nextCreditId = 1;
|
|
173
|
+
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
174
|
+
if (amount <= 0n) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"MemoryPointLedger: pending credit amount must be positive"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (durationMs <= 0) {
|
|
180
|
+
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
181
|
+
}
|
|
182
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
183
|
+
const lockId = `credit-${this.nextCreditId++}`;
|
|
184
|
+
const now = this.now();
|
|
185
|
+
this.pendingCredits.set(lockId, {
|
|
186
|
+
lockId,
|
|
187
|
+
userAddress: user,
|
|
188
|
+
amount,
|
|
189
|
+
tokenAddress: tokenAddress !== void 0 ? (0, import_viem.getAddress)(tokenAddress) : void 0,
|
|
190
|
+
createdAt: now,
|
|
191
|
+
expiresAt: now + durationMs,
|
|
192
|
+
status: "PENDING"
|
|
193
|
+
});
|
|
194
|
+
return lockId;
|
|
195
|
+
}
|
|
196
|
+
async resolveCreditByBurnTx(lockId, txHash) {
|
|
197
|
+
const credit = this.pendingCredits.get(lockId);
|
|
198
|
+
if (!credit) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (credit.status === "RESOLVED") {
|
|
204
|
+
if (credit.txHash === txHash) return;
|
|
205
|
+
throw new Error(
|
|
206
|
+
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const token = normalizeToken(credit.tokenAddress);
|
|
210
|
+
const key = balanceKey(credit.userAddress, token);
|
|
211
|
+
const current = this.balances.get(key) ?? 0n;
|
|
212
|
+
this.balances.set(key, current + credit.amount);
|
|
213
|
+
credit.status = "RESOLVED";
|
|
214
|
+
credit.txHash = txHash;
|
|
215
|
+
}
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
154
217
|
// Internal helpers
|
|
155
218
|
// -------------------------------------------------------------------------
|
|
156
219
|
/**
|
|
@@ -165,16 +228,23 @@ var MemoryPointLedger = class {
|
|
|
165
228
|
}
|
|
166
229
|
}
|
|
167
230
|
}
|
|
168
|
-
lockedTotalFor(userAddress) {
|
|
231
|
+
lockedTotalFor(userAddress, tokenKey) {
|
|
169
232
|
let total = 0n;
|
|
170
233
|
for (const lock of this.locks.values()) {
|
|
171
|
-
if (lock.userAddress === userAddress && lock.status === "PENDING") {
|
|
234
|
+
if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
|
|
172
235
|
total += lock.amount;
|
|
173
236
|
}
|
|
174
237
|
}
|
|
175
238
|
return total;
|
|
176
239
|
}
|
|
177
240
|
};
|
|
241
|
+
var DEFAULT_TOKEN_KEY = "default";
|
|
242
|
+
function normalizeToken(tokenAddress) {
|
|
243
|
+
return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : (0, import_viem.getAddress)(tokenAddress);
|
|
244
|
+
}
|
|
245
|
+
function balanceKey(user, tokenKey) {
|
|
246
|
+
return `${user}|${tokenKey}`;
|
|
247
|
+
}
|
|
178
248
|
|
|
179
249
|
// src/policy/defaultPolicy.ts
|
|
180
250
|
var DefaultPolicyEngine = class {
|
|
@@ -196,7 +266,10 @@ var DefaultPolicyEngine = class {
|
|
|
196
266
|
if (request.amount <= 0n) {
|
|
197
267
|
return { approved: false, reason: "Amount must be positive" };
|
|
198
268
|
}
|
|
199
|
-
const available = await this.ledger.getBalance(
|
|
269
|
+
const available = await this.ledger.getBalance(
|
|
270
|
+
request.userAddress,
|
|
271
|
+
request.pointTokenAddress
|
|
272
|
+
);
|
|
200
273
|
if (available < request.amount) {
|
|
201
274
|
return {
|
|
202
275
|
approved: false,
|
|
@@ -609,6 +682,11 @@ var RelayService = class {
|
|
|
609
682
|
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
610
683
|
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
611
684
|
* need manual review because the tx may still land).
|
|
685
|
+
*
|
|
686
|
+
* @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
|
|
687
|
+
* `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
|
|
688
|
+
* still needs to finalize Relayer v2 ABI before the replacements
|
|
689
|
+
* can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
|
|
612
690
|
*/
|
|
613
691
|
async submitMintAndSwap(params) {
|
|
614
692
|
if (this.simulateBeforeSubmit && this.provider) {
|
|
@@ -687,84 +765,35 @@ var DEFAULT_GAS_UNITS = 500000n;
|
|
|
687
765
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
688
766
|
var FeeManager = class {
|
|
689
767
|
provider;
|
|
690
|
-
|
|
691
|
-
mintAndSwapGasUnits;
|
|
768
|
+
gasUnits;
|
|
692
769
|
gasPremiumBps;
|
|
693
|
-
|
|
694
|
-
rebalanceThresholdWei;
|
|
695
|
-
rebalanceUsdtAmount;
|
|
696
|
-
swapUsdtToNative;
|
|
770
|
+
quoteNativeToFee;
|
|
697
771
|
constructor(config) {
|
|
698
772
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
699
|
-
if (!config.
|
|
700
|
-
throw new Error("FeeManager:
|
|
701
|
-
if (!config.quoteNativeToUsdt)
|
|
702
|
-
throw new Error("FeeManager: quoteNativeToUsdt required");
|
|
773
|
+
if (!config.quoteNativeToFee)
|
|
774
|
+
throw new Error("FeeManager: quoteNativeToFee required");
|
|
703
775
|
this.provider = config.provider;
|
|
704
|
-
this.
|
|
705
|
-
this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
|
|
776
|
+
this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
|
|
706
777
|
this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
|
|
707
|
-
this.
|
|
708
|
-
if (config.rebalanceThresholdWei !== void 0) {
|
|
709
|
-
this.rebalanceThresholdWei = config.rebalanceThresholdWei;
|
|
710
|
-
}
|
|
711
|
-
if (config.rebalanceUsdtAmount !== void 0) {
|
|
712
|
-
this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
|
|
713
|
-
}
|
|
714
|
-
if (config.swapUsdtToNative) {
|
|
715
|
-
this.swapUsdtToNative = config.swapUsdtToNative;
|
|
716
|
-
}
|
|
717
|
-
const rebalanceFields = [
|
|
718
|
-
config.rebalanceThresholdWei,
|
|
719
|
-
config.rebalanceUsdtAmount,
|
|
720
|
-
config.swapUsdtToNative
|
|
721
|
-
];
|
|
722
|
-
const someSet = rebalanceFields.some((v) => v !== void 0);
|
|
723
|
-
const allSet = rebalanceFields.every((v) => v !== void 0);
|
|
724
|
-
if (someSet && !allSet) {
|
|
725
|
-
throw new Error(
|
|
726
|
-
"FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
|
|
727
|
-
);
|
|
728
|
-
}
|
|
778
|
+
this.quoteNativeToFee = config.quoteNativeToFee;
|
|
729
779
|
}
|
|
730
780
|
/**
|
|
731
|
-
* Estimate the
|
|
732
|
-
*
|
|
781
|
+
* Estimate the fee (in the caller's fee currency) to charge for the
|
|
782
|
+
* next sponsored UserOp:
|
|
783
|
+
*
|
|
784
|
+
* nativeCost = gasUnits × gasPrice
|
|
785
|
+
* withPremium = nativeCost × premiumBps / 10_000
|
|
786
|
+
* fee = quoteNativeToFee(withPremium)
|
|
733
787
|
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
788
|
+
* For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
|
|
789
|
+
* from the response, the name `estimateGasFee` is kept — but the
|
|
790
|
+
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
737
791
|
*/
|
|
738
792
|
async estimateGasFee() {
|
|
739
793
|
const gasPrice = await this.provider.getGasPrice();
|
|
740
|
-
const nativeCost = gasPrice * this.
|
|
794
|
+
const nativeCost = gasPrice * this.gasUnits;
|
|
741
795
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
742
|
-
return this.
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
|
-
* Check the operator's native balance and, if it has dropped below the
|
|
746
|
-
* configured threshold, trigger a USDT→native rebalance via the injected
|
|
747
|
-
* `swapUsdtToNative` function.
|
|
748
|
-
*
|
|
749
|
-
* Returns `true` if a rebalance was performed, `false` otherwise.
|
|
750
|
-
* Silently no-ops when rebalance is not configured.
|
|
751
|
-
*/
|
|
752
|
-
async rebalanceIfNeeded() {
|
|
753
|
-
if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
const operatorAddress = this.operatorWallet.account?.address;
|
|
757
|
-
if (!operatorAddress) {
|
|
758
|
-
throw new Error(
|
|
759
|
-
"FeeManager: operator wallet has no account attached \u2014 cannot read balance"
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
const balance = await this.provider.getBalance({ address: operatorAddress });
|
|
763
|
-
if (balance >= this.rebalanceThresholdWei) {
|
|
764
|
-
return false;
|
|
765
|
-
}
|
|
766
|
-
await this.swapUsdtToNative(this.rebalanceUsdtAmount);
|
|
767
|
-
return true;
|
|
796
|
+
return this.quoteNativeToFee(withPremium);
|
|
768
797
|
}
|
|
769
798
|
};
|
|
770
799
|
|
|
@@ -810,6 +839,12 @@ var MintingGateway = class {
|
|
|
810
839
|
this.now = config.now ?? (() => Date.now());
|
|
811
840
|
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
812
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* @deprecated Since 0.3.0 — will be renamed to `processMint()` once
|
|
844
|
+
* the SC team finalizes Relayer v2 ABI. The new flow drops the
|
|
845
|
+
* swap steps entirely (no more single-call mint+swap); users swap
|
|
846
|
+
* separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
|
|
847
|
+
*/
|
|
813
848
|
async processMintAndCashOut(request) {
|
|
814
849
|
const { receiverConsent, receiverSignature } = request;
|
|
815
850
|
if (!receiverConsent || !receiverSignature) {
|
|
@@ -874,7 +909,8 @@ var MintingGateway = class {
|
|
|
874
909
|
lockId = await this.ledger.lockForMinting(
|
|
875
910
|
request.userAddress,
|
|
876
911
|
receiverConsent.amount,
|
|
877
|
-
lockDurationMs
|
|
912
|
+
lockDurationMs,
|
|
913
|
+
request.pointTokenAddress
|
|
878
914
|
);
|
|
879
915
|
} catch (err) {
|
|
880
916
|
throw new MintingGatewayError(
|
|
@@ -1164,11 +1200,19 @@ var PointIndexer = class {
|
|
|
1164
1200
|
* issuer to mint without going through the gateway.
|
|
1165
1201
|
*/
|
|
1166
1202
|
async finalize(evt) {
|
|
1167
|
-
const locks = await this.ledger.getLockedRequests(
|
|
1203
|
+
const locks = await this.ledger.getLockedRequests(
|
|
1204
|
+
evt.to,
|
|
1205
|
+
this.pointTokenAddress
|
|
1206
|
+
);
|
|
1168
1207
|
const match = pickMatchingLock(locks, evt.amount);
|
|
1169
1208
|
if (!match) return;
|
|
1170
1209
|
try {
|
|
1171
|
-
await this.ledger.deductBalance(
|
|
1210
|
+
await this.ledger.deductBalance(
|
|
1211
|
+
evt.to,
|
|
1212
|
+
evt.amount,
|
|
1213
|
+
evt.txHash,
|
|
1214
|
+
this.pointTokenAddress
|
|
1215
|
+
);
|
|
1172
1216
|
} catch {
|
|
1173
1217
|
return;
|
|
1174
1218
|
}
|
|
@@ -1190,17 +1234,177 @@ function pickMatchingLock(locks, amount) {
|
|
|
1190
1234
|
return best;
|
|
1191
1235
|
}
|
|
1192
1236
|
|
|
1193
|
-
// src/
|
|
1237
|
+
// src/indexer/burnIndexer.ts
|
|
1194
1238
|
var import_viem6 = require("viem");
|
|
1239
|
+
var TRANSFER_EVENT2 = (0, import_viem6.parseAbiItem)(
|
|
1240
|
+
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1241
|
+
);
|
|
1242
|
+
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
1243
|
+
var DEFAULT_CONFIRMATIONS2 = 3;
|
|
1244
|
+
var DEFAULT_BATCH_SIZE2 = 2000n;
|
|
1245
|
+
var DEFAULT_POLL_INTERVAL_MS2 = 5e3;
|
|
1246
|
+
var BurnIndexer = class {
|
|
1247
|
+
provider;
|
|
1248
|
+
pointTokenAddress;
|
|
1249
|
+
ledger;
|
|
1250
|
+
cursorStore;
|
|
1251
|
+
startBlock;
|
|
1252
|
+
confirmations;
|
|
1253
|
+
batchSize;
|
|
1254
|
+
pollIntervalMs;
|
|
1255
|
+
/**
|
|
1256
|
+
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1257
|
+
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1258
|
+
* ledger's query path.
|
|
1259
|
+
*
|
|
1260
|
+
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1261
|
+
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1262
|
+
* incrementing IDs so callers with the memory ledger must provide a
|
|
1263
|
+
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1264
|
+
* their `pending_credits` table.
|
|
1265
|
+
*/
|
|
1266
|
+
matchLockId = async () => void 0;
|
|
1267
|
+
running = false;
|
|
1268
|
+
timer;
|
|
1269
|
+
constructor(config) {
|
|
1270
|
+
if (!config.provider) throw new Error("BurnIndexer: provider required");
|
|
1271
|
+
if (!config.pointTokenAddress)
|
|
1272
|
+
throw new Error("BurnIndexer: pointTokenAddress required");
|
|
1273
|
+
if (!config.ledger) throw new Error("BurnIndexer: ledger required");
|
|
1274
|
+
this.provider = config.provider;
|
|
1275
|
+
this.pointTokenAddress = config.pointTokenAddress;
|
|
1276
|
+
this.ledger = config.ledger;
|
|
1277
|
+
this.cursorStore = config.cursorStore ?? new InMemoryCursorStore();
|
|
1278
|
+
this.startBlock = config.fromBlock ?? 0n;
|
|
1279
|
+
this.confirmations = BigInt(
|
|
1280
|
+
config.confirmations ?? DEFAULT_CONFIRMATIONS2
|
|
1281
|
+
);
|
|
1282
|
+
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1283
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
1284
|
+
}
|
|
1285
|
+
start() {
|
|
1286
|
+
if (this.running) return;
|
|
1287
|
+
this.running = true;
|
|
1288
|
+
void this.tick();
|
|
1289
|
+
}
|
|
1290
|
+
stop() {
|
|
1291
|
+
this.running = false;
|
|
1292
|
+
if (this.timer) {
|
|
1293
|
+
clearTimeout(this.timer);
|
|
1294
|
+
this.timer = void 0;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async tick() {
|
|
1298
|
+
if (!this.running) return;
|
|
1299
|
+
try {
|
|
1300
|
+
const latest = await this.provider.getBlockNumber();
|
|
1301
|
+
const safeHead = latest - this.confirmations;
|
|
1302
|
+
if (safeHead < 0n) {
|
|
1303
|
+
this.scheduleNext();
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const stored = await this.cursorStore.load();
|
|
1307
|
+
const from = stored ?? this.startBlock;
|
|
1308
|
+
if (from > safeHead) {
|
|
1309
|
+
this.scheduleNext();
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
await this.processBlockRange(from, safeHead);
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
this.scheduleNext();
|
|
1316
|
+
}
|
|
1317
|
+
scheduleNext() {
|
|
1318
|
+
if (!this.running) return;
|
|
1319
|
+
this.timer = setTimeout(() => void this.tick(), this.pollIntervalMs);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Scan `[from, to]` inclusive for burn events. Callers can drive this
|
|
1323
|
+
* directly to backfill a specific range without `start()`. Cursor is
|
|
1324
|
+
* advanced to `to + 1` on completion.
|
|
1325
|
+
*/
|
|
1326
|
+
async processBlockRange(from, to) {
|
|
1327
|
+
if (from > to) return;
|
|
1328
|
+
let cursor = from;
|
|
1329
|
+
while (cursor <= to) {
|
|
1330
|
+
const chunkEnd = cursor + this.batchSize - 1n > to ? to : cursor + this.batchSize - 1n;
|
|
1331
|
+
const logs = await this.provider.getLogs({
|
|
1332
|
+
address: this.pointTokenAddress,
|
|
1333
|
+
event: TRANSFER_EVENT2,
|
|
1334
|
+
args: { to: ZERO_ADDRESS2 },
|
|
1335
|
+
// filter: burn = transfer to zero
|
|
1336
|
+
fromBlock: cursor,
|
|
1337
|
+
toBlock: chunkEnd
|
|
1338
|
+
});
|
|
1339
|
+
const events = this.decodeBurnEvents(logs);
|
|
1340
|
+
events.sort((a, b) => {
|
|
1341
|
+
if (a.blockNumber !== b.blockNumber) {
|
|
1342
|
+
return a.blockNumber < b.blockNumber ? -1 : 1;
|
|
1343
|
+
}
|
|
1344
|
+
return a.logIndex - b.logIndex;
|
|
1345
|
+
});
|
|
1346
|
+
for (const evt of events) {
|
|
1347
|
+
await this.finalize(evt);
|
|
1348
|
+
}
|
|
1349
|
+
await this.cursorStore.save(chunkEnd + 1n);
|
|
1350
|
+
cursor = chunkEnd + 1n;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
decodeBurnEvents(logs) {
|
|
1354
|
+
const out = [];
|
|
1355
|
+
for (const log of logs) {
|
|
1356
|
+
const args = log.args;
|
|
1357
|
+
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1358
|
+
if ((0, import_viem6.getAddress)(args.to) !== ZERO_ADDRESS2) continue;
|
|
1359
|
+
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1360
|
+
out.push({
|
|
1361
|
+
from: (0, import_viem6.getAddress)(args.from),
|
|
1362
|
+
amount: args.value,
|
|
1363
|
+
blockNumber: log.blockNumber,
|
|
1364
|
+
txHash: log.transactionHash,
|
|
1365
|
+
logIndex: log.logIndex ?? 0
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return out;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Resolve a matching pending credit for this burn event and call
|
|
1372
|
+
* `ledger.resolveCreditByBurnTx(lockId, txHash)`. If no match found,
|
|
1373
|
+
* log + skip.
|
|
1374
|
+
*/
|
|
1375
|
+
async finalize(evt) {
|
|
1376
|
+
const lockId = await this.matchLockId(evt);
|
|
1377
|
+
if (!lockId) return;
|
|
1378
|
+
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
// src/api/handlers.ts
|
|
1389
|
+
var import_viem7 = require("viem");
|
|
1195
1390
|
var import_core5 = require("@pafi-dev/core");
|
|
1196
1391
|
var IssuerApiHandlers = class {
|
|
1197
1392
|
authService;
|
|
1198
1393
|
gateway;
|
|
1199
1394
|
ledger;
|
|
1200
1395
|
provider;
|
|
1201
|
-
|
|
1396
|
+
/**
|
|
1397
|
+
* Set of supported PointToken addresses (checksum-normalized). Handlers
|
|
1398
|
+
* validate the request's `pointTokenAddress` against this set.
|
|
1399
|
+
*/
|
|
1400
|
+
supportedTokens;
|
|
1401
|
+
/** First supported token — used as default when a handler doesn't
|
|
1402
|
+
* receive a `pointTokenAddress` in the request (shouldn't happen in
|
|
1403
|
+
* practice, but keeps type-narrowing happy). */
|
|
1404
|
+
defaultToken;
|
|
1202
1405
|
chainId;
|
|
1203
1406
|
contracts;
|
|
1407
|
+
pafiWebUrl;
|
|
1204
1408
|
feeManager;
|
|
1205
1409
|
poolsProvider;
|
|
1206
1410
|
constructor(config) {
|
|
@@ -1208,9 +1412,18 @@ var IssuerApiHandlers = class {
|
|
|
1208
1412
|
this.gateway = config.gateway;
|
|
1209
1413
|
this.ledger = config.ledger;
|
|
1210
1414
|
this.provider = config.provider;
|
|
1211
|
-
|
|
1415
|
+
const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
1416
|
+
if (raw.length === 0) {
|
|
1417
|
+
throw new Error(
|
|
1418
|
+
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
const normalized = raw.map((a) => (0, import_viem7.getAddress)(a));
|
|
1422
|
+
this.supportedTokens = new Set(normalized);
|
|
1423
|
+
this.defaultToken = normalized[0];
|
|
1212
1424
|
this.chainId = config.chainId;
|
|
1213
1425
|
this.contracts = config.contracts;
|
|
1426
|
+
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1214
1427
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1215
1428
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1216
1429
|
}
|
|
@@ -1246,7 +1459,16 @@ var IssuerApiHandlers = class {
|
|
|
1246
1459
|
`handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
|
|
1247
1460
|
);
|
|
1248
1461
|
}
|
|
1249
|
-
|
|
1462
|
+
const contracts = {
|
|
1463
|
+
...this.contracts,
|
|
1464
|
+
pointTokens: Array.from(this.supportedTokens)
|
|
1465
|
+
};
|
|
1466
|
+
const response = {
|
|
1467
|
+
chainId: this.chainId,
|
|
1468
|
+
contracts
|
|
1469
|
+
};
|
|
1470
|
+
if (this.pafiWebUrl) response.pafiWebUrl = this.pafiWebUrl;
|
|
1471
|
+
return response;
|
|
1250
1472
|
}
|
|
1251
1473
|
/** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
|
|
1252
1474
|
async handleGasFee() {
|
|
@@ -1297,15 +1519,15 @@ var IssuerApiHandlers = class {
|
|
|
1297
1519
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1298
1520
|
);
|
|
1299
1521
|
}
|
|
1300
|
-
const normalizedAuthed = (0,
|
|
1301
|
-
const normalizedRequest = (0,
|
|
1522
|
+
const normalizedAuthed = (0, import_viem7.getAddress)(userAddress);
|
|
1523
|
+
const normalizedRequest = (0, import_viem7.getAddress)(request.userAddress);
|
|
1302
1524
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1303
1525
|
throw new Error(
|
|
1304
1526
|
"handleUser: request userAddress must match authenticated user"
|
|
1305
1527
|
);
|
|
1306
1528
|
}
|
|
1307
|
-
const pointToken = (0,
|
|
1308
|
-
if (
|
|
1529
|
+
const pointToken = (0, import_viem7.getAddress)(request.pointTokenAddress);
|
|
1530
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1309
1531
|
throw new Error(
|
|
1310
1532
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
1311
1533
|
);
|
|
@@ -1313,7 +1535,7 @@ var IssuerApiHandlers = class {
|
|
|
1313
1535
|
const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
|
|
1314
1536
|
(0, import_core5.getMintRequestNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1315
1537
|
(0, import_core5.getReceiverConsentNonce)(this.provider, pointToken, normalizedAuthed),
|
|
1316
|
-
this.ledger.getBalance(normalizedAuthed),
|
|
1538
|
+
this.ledger.getBalance(normalizedAuthed, pointToken),
|
|
1317
1539
|
(0, import_core5.getPointTokenBalance)(this.provider, pointToken, normalizedAuthed),
|
|
1318
1540
|
(0, import_core5.isMinter)(this.provider, pointToken, normalizedAuthed)
|
|
1319
1541
|
]);
|
|
@@ -1347,8 +1569,8 @@ var IssuerApiHandlers = class {
|
|
|
1347
1569
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1348
1570
|
);
|
|
1349
1571
|
}
|
|
1350
|
-
const pointToken = (0,
|
|
1351
|
-
if (
|
|
1572
|
+
const pointToken = (0, import_viem7.getAddress)(request.pointTokenAddress);
|
|
1573
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1352
1574
|
throw new Error(
|
|
1353
1575
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
1354
1576
|
);
|
|
@@ -1372,8 +1594,14 @@ var IssuerApiHandlers = class {
|
|
|
1372
1594
|
/**
|
|
1373
1595
|
* `POST /claim-and-swap`
|
|
1374
1596
|
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1597
|
+
* @deprecated Since 0.3.0 — the single-call mint-then-swap flow is
|
|
1598
|
+
* retired in v1.4. Use the new `handleClaim()` (mint only) and let
|
|
1599
|
+
* the user swap separately on PAFI Web. See
|
|
1600
|
+
* [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
|
|
1601
|
+
* removed in 2.0.
|
|
1602
|
+
*
|
|
1603
|
+
* Legacy behavior: the terminal handler forwards the verified
|
|
1604
|
+
* consent to the MintingGateway, which runs the 11-step flow.
|
|
1377
1605
|
*/
|
|
1378
1606
|
async handleClaimAndSwap(userAddress, request) {
|
|
1379
1607
|
if (request.chainId !== this.chainId) {
|
|
@@ -1381,14 +1609,14 @@ var IssuerApiHandlers = class {
|
|
|
1381
1609
|
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1382
1610
|
);
|
|
1383
1611
|
}
|
|
1384
|
-
const pointToken = (0,
|
|
1385
|
-
if (
|
|
1612
|
+
const pointToken = (0, import_viem7.getAddress)(request.pointTokenAddress);
|
|
1613
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1386
1614
|
throw new Error(
|
|
1387
1615
|
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1388
1616
|
);
|
|
1389
1617
|
}
|
|
1390
1618
|
const result = await this.gateway.processMintAndCashOut({
|
|
1391
|
-
userAddress: (0,
|
|
1619
|
+
userAddress: (0, import_viem7.getAddress)(userAddress),
|
|
1392
1620
|
pointTokenAddress: pointToken,
|
|
1393
1621
|
chainId: request.chainId,
|
|
1394
1622
|
domain: request.domain,
|
|
@@ -1619,7 +1847,274 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1619
1847
|
return BigInt(whole + padded);
|
|
1620
1848
|
}
|
|
1621
1849
|
|
|
1850
|
+
// src/balance/balanceAggregator.ts
|
|
1851
|
+
var import_core6 = require("@pafi-dev/core");
|
|
1852
|
+
var BalanceAggregator = class {
|
|
1853
|
+
provider;
|
|
1854
|
+
ledger;
|
|
1855
|
+
constructor(config) {
|
|
1856
|
+
if (!config.provider) {
|
|
1857
|
+
throw new Error("BalanceAggregator: provider is required");
|
|
1858
|
+
}
|
|
1859
|
+
if (!config.ledger) {
|
|
1860
|
+
throw new Error("BalanceAggregator: ledger is required");
|
|
1861
|
+
}
|
|
1862
|
+
this.provider = config.provider;
|
|
1863
|
+
this.ledger = config.ledger;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Combined balance for a single (user, token) pair. Fetches off-chain
|
|
1867
|
+
* + on-chain in parallel.
|
|
1868
|
+
*/
|
|
1869
|
+
async getCombinedBalance(user, pointToken) {
|
|
1870
|
+
const [offChain, onChain] = await Promise.all([
|
|
1871
|
+
this.ledger.getBalance(user, pointToken),
|
|
1872
|
+
(0, import_core6.getPointTokenBalance)(this.provider, pointToken, user)
|
|
1873
|
+
]);
|
|
1874
|
+
return {
|
|
1875
|
+
offChain,
|
|
1876
|
+
onChain,
|
|
1877
|
+
total: offChain + onChain
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Combined balance for multiple tokens owned by the same user. Runs
|
|
1882
|
+
* all lookups in parallel. Returns a Map keyed by the token address
|
|
1883
|
+
* (same casing as supplied — caller should normalize if needed).
|
|
1884
|
+
*/
|
|
1885
|
+
async getCombinedBalanceMulti(user, pointTokens) {
|
|
1886
|
+
const entries = await Promise.all(
|
|
1887
|
+
pointTokens.map(async (token) => {
|
|
1888
|
+
const balance = await this.getCombinedBalance(user, token);
|
|
1889
|
+
return [token, balance];
|
|
1890
|
+
})
|
|
1891
|
+
);
|
|
1892
|
+
return new Map(entries);
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
// src/pafi-backend/types.ts
|
|
1897
|
+
var PafiBackendError = class extends Error {
|
|
1898
|
+
constructor(code, message, httpStatus, details, opts) {
|
|
1899
|
+
super(message);
|
|
1900
|
+
this.code = code;
|
|
1901
|
+
this.httpStatus = httpStatus;
|
|
1902
|
+
this.details = details;
|
|
1903
|
+
this.name = "PafiBackendError";
|
|
1904
|
+
if (opts?.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
|
|
1905
|
+
if (opts?.safeToRetry !== void 0) this.serverSafeToRetry = opts.safeToRetry;
|
|
1906
|
+
}
|
|
1907
|
+
code;
|
|
1908
|
+
httpStatus;
|
|
1909
|
+
details;
|
|
1910
|
+
/**
|
|
1911
|
+
* Seconds to wait before retry. Populated from the server body
|
|
1912
|
+
* (e.g. rate limit returns the number of seconds until UTC midnight).
|
|
1913
|
+
*/
|
|
1914
|
+
retryAfter;
|
|
1915
|
+
/**
|
|
1916
|
+
* `safeToRetry` as reported by the server body. Prefer this over the
|
|
1917
|
+
* code-based heuristic when available — the server knows more about
|
|
1918
|
+
* whether the same request will succeed on retry.
|
|
1919
|
+
*/
|
|
1920
|
+
serverSafeToRetry;
|
|
1921
|
+
/**
|
|
1922
|
+
* Whether the caller can safely retry the same request.
|
|
1923
|
+
*
|
|
1924
|
+
* If the server provided `safeToRetry` in the body, trust that.
|
|
1925
|
+
* Otherwise fall back to a code-based heuristic.
|
|
1926
|
+
*/
|
|
1927
|
+
get safeToRetry() {
|
|
1928
|
+
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
1929
|
+
switch (this.code) {
|
|
1930
|
+
case "PAYMASTER_UNAVAILABLE":
|
|
1931
|
+
case "PAYMASTER_TIMEOUT":
|
|
1932
|
+
case "RATE_LIMITER_UNAVAILABLE":
|
|
1933
|
+
case "INTERNAL_ERROR":
|
|
1934
|
+
case "TIMEOUT":
|
|
1935
|
+
case "NETWORK_ERROR":
|
|
1936
|
+
return true;
|
|
1937
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
1938
|
+
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
1939
|
+
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
1940
|
+
return true;
|
|
1941
|
+
// after retryAfter
|
|
1942
|
+
default:
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1948
|
+
// src/pafi-backend/pafiBackendClient.ts
|
|
1949
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1950
|
+
var RETRY_DEFAULTS = {
|
|
1951
|
+
maxAttempts: 1,
|
|
1952
|
+
initialDelayMs: 500,
|
|
1953
|
+
maxDelayMs: 1e4,
|
|
1954
|
+
maxRetryAfterMs: 3e4
|
|
1955
|
+
};
|
|
1956
|
+
var PafiBackendClient = class {
|
|
1957
|
+
url;
|
|
1958
|
+
issuerId;
|
|
1959
|
+
apiKey;
|
|
1960
|
+
fetchImpl;
|
|
1961
|
+
timeoutMs;
|
|
1962
|
+
retry;
|
|
1963
|
+
constructor(config) {
|
|
1964
|
+
if (!config.url) {
|
|
1965
|
+
throw new Error("PafiBackendClient: url is required");
|
|
1966
|
+
}
|
|
1967
|
+
if (!config.issuerId) {
|
|
1968
|
+
throw new Error("PafiBackendClient: issuerId is required");
|
|
1969
|
+
}
|
|
1970
|
+
if (!config.apiKey) {
|
|
1971
|
+
throw new Error("PafiBackendClient: apiKey is required");
|
|
1972
|
+
}
|
|
1973
|
+
this.url = config.url.replace(/\/+$/, "");
|
|
1974
|
+
this.issuerId = config.issuerId;
|
|
1975
|
+
this.apiKey = config.apiKey;
|
|
1976
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1977
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1978
|
+
this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
|
|
1979
|
+
if (!this.fetchImpl) {
|
|
1980
|
+
throw new Error(
|
|
1981
|
+
"PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
if (this.retry.maxAttempts < 1) {
|
|
1985
|
+
throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Request paymaster sponsorship for a pre-built UserOperation.
|
|
1990
|
+
* See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
|
|
1991
|
+
*
|
|
1992
|
+
* Retries automatically on transient failures (5xx, timeouts, network
|
|
1993
|
+
* errors, and errors the server flags with `safeToRetry: true`) up to
|
|
1994
|
+
* `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
|
|
1995
|
+
*
|
|
1996
|
+
* @throws PafiBackendError on final failure after exhausting retries
|
|
1997
|
+
*/
|
|
1998
|
+
async requestSponsorship(req) {
|
|
1999
|
+
return this.postWithRetry(
|
|
2000
|
+
"/paymaster/sponsor",
|
|
2001
|
+
req
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
// -------------------------------------------------------------------------
|
|
2005
|
+
// Internals
|
|
2006
|
+
// -------------------------------------------------------------------------
|
|
2007
|
+
async postWithRetry(path, body) {
|
|
2008
|
+
let lastError;
|
|
2009
|
+
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
2010
|
+
try {
|
|
2011
|
+
return await this.post(path, body);
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
if (!(err instanceof PafiBackendError)) throw err;
|
|
2014
|
+
lastError = err;
|
|
2015
|
+
const isLastAttempt = attempt >= this.retry.maxAttempts;
|
|
2016
|
+
if (isLastAttempt || !err.safeToRetry) throw err;
|
|
2017
|
+
const delay = this.computeBackoff(attempt, err.retryAfter);
|
|
2018
|
+
if (delay === null) throw err;
|
|
2019
|
+
await this.sleep(delay);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
throw lastError;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Pick the delay before the next retry.
|
|
2026
|
+
* - If the server sent `retryAfter` (seconds), honor it (capped by
|
|
2027
|
+
* `maxRetryAfterMs`) — returns null if the server wait exceeds the
|
|
2028
|
+
* cap, signalling the caller should give up.
|
|
2029
|
+
* - Otherwise: exponential backoff with ±20% jitter, capped at
|
|
2030
|
+
* `maxDelayMs`.
|
|
2031
|
+
*/
|
|
2032
|
+
computeBackoff(attempt, retryAfter) {
|
|
2033
|
+
if (retryAfter !== void 0) {
|
|
2034
|
+
const serverMs = retryAfter * 1e3;
|
|
2035
|
+
if (serverMs > this.retry.maxRetryAfterMs) return null;
|
|
2036
|
+
return serverMs;
|
|
2037
|
+
}
|
|
2038
|
+
const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
|
|
2039
|
+
const capped = Math.min(exp, this.retry.maxDelayMs);
|
|
2040
|
+
const jitter = capped * (0.8 + Math.random() * 0.4);
|
|
2041
|
+
return Math.round(jitter);
|
|
2042
|
+
}
|
|
2043
|
+
sleep(ms) {
|
|
2044
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2045
|
+
}
|
|
2046
|
+
async post(path, body) {
|
|
2047
|
+
const controller = new AbortController();
|
|
2048
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2049
|
+
let response;
|
|
2050
|
+
try {
|
|
2051
|
+
response = await this.fetchImpl(`${this.url}${path}`, {
|
|
2052
|
+
method: "POST",
|
|
2053
|
+
headers: {
|
|
2054
|
+
"Content-Type": "application/json",
|
|
2055
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2056
|
+
"X-Issuer-Id": this.issuerId
|
|
2057
|
+
},
|
|
2058
|
+
body: JSON.stringify(body, this.bigintReplacer),
|
|
2059
|
+
signal: controller.signal
|
|
2060
|
+
});
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
if (err.name === "AbortError") {
|
|
2063
|
+
throw new PafiBackendError(
|
|
2064
|
+
"TIMEOUT",
|
|
2065
|
+
`PAFI Backend request timed out after ${this.timeoutMs}ms`,
|
|
2066
|
+
0
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
throw new PafiBackendError(
|
|
2070
|
+
"NETWORK_ERROR",
|
|
2071
|
+
`PAFI Backend unreachable: ${err.message}`,
|
|
2072
|
+
0
|
|
2073
|
+
);
|
|
2074
|
+
} finally {
|
|
2075
|
+
clearTimeout(timeoutId);
|
|
2076
|
+
}
|
|
2077
|
+
const text = await response.text();
|
|
2078
|
+
if (!response.ok) {
|
|
2079
|
+
let code = "INTERNAL_ERROR";
|
|
2080
|
+
let message = text || response.statusText;
|
|
2081
|
+
let details;
|
|
2082
|
+
let retryAfter;
|
|
2083
|
+
let serverSafeToRetry;
|
|
2084
|
+
try {
|
|
2085
|
+
const parsed = JSON.parse(text);
|
|
2086
|
+
code = parsed.code ?? code;
|
|
2087
|
+
message = parsed.message ?? message;
|
|
2088
|
+
details = parsed.details;
|
|
2089
|
+
if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
|
|
2090
|
+
if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
|
|
2091
|
+
} catch {
|
|
2092
|
+
}
|
|
2093
|
+
throw new PafiBackendError(code, message, response.status, details, {
|
|
2094
|
+
...retryAfter !== void 0 ? { retryAfter } : {},
|
|
2095
|
+
...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
return JSON.parse(text, this.bigintReviver);
|
|
2099
|
+
}
|
|
2100
|
+
/** JSON replacer that stringifies bigints. Paired with bigintReviver. */
|
|
2101
|
+
bigintReplacer = (_key, value) => {
|
|
2102
|
+
return typeof value === "bigint" ? value.toString() : value;
|
|
2103
|
+
};
|
|
2104
|
+
/**
|
|
2105
|
+
* JSON reviver that coerces specific numeric-string fields back to
|
|
2106
|
+
* bigint. The server must send these fields as decimal strings.
|
|
2107
|
+
*/
|
|
2108
|
+
bigintReviver = (key, value) => {
|
|
2109
|
+
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)) {
|
|
2110
|
+
return BigInt(value);
|
|
2111
|
+
}
|
|
2112
|
+
return value;
|
|
2113
|
+
};
|
|
2114
|
+
};
|
|
2115
|
+
|
|
1622
2116
|
// src/config.ts
|
|
2117
|
+
var import_viem8 = require("viem");
|
|
1623
2118
|
function createIssuerService(config) {
|
|
1624
2119
|
if (!config.provider) {
|
|
1625
2120
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -1630,9 +2125,6 @@ function createIssuerService(config) {
|
|
|
1630
2125
|
if (!config.signer) {
|
|
1631
2126
|
throw new Error("createIssuerService: signer is required");
|
|
1632
2127
|
}
|
|
1633
|
-
if (!config.pointTokenAddress) {
|
|
1634
|
-
throw new Error("createIssuerService: pointTokenAddress is required");
|
|
1635
|
-
}
|
|
1636
2128
|
if (!config.relayAddress) {
|
|
1637
2129
|
throw new Error("createIssuerService: relayAddress is required");
|
|
1638
2130
|
}
|
|
@@ -1642,6 +2134,13 @@ function createIssuerService(config) {
|
|
|
1642
2134
|
if (!config.auth?.domain) {
|
|
1643
2135
|
throw new Error("createIssuerService: auth.domain is required");
|
|
1644
2136
|
}
|
|
2137
|
+
const rawAddresses = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
2138
|
+
if (rawAddresses.length === 0) {
|
|
2139
|
+
throw new Error(
|
|
2140
|
+
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
const tokenAddresses = rawAddresses.map((a) => (0, import_viem8.getAddress)(a));
|
|
1645
2144
|
const ledger = config.ledger ?? new MemoryPointLedger();
|
|
1646
2145
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1647
2146
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
@@ -1671,8 +2170,7 @@ function createIssuerService(config) {
|
|
|
1671
2170
|
if (config.fee) {
|
|
1672
2171
|
feeManager = new FeeManager({
|
|
1673
2172
|
...config.fee,
|
|
1674
|
-
provider: config.provider
|
|
1675
|
-
operatorWallet: config.operatorWallet
|
|
2173
|
+
provider: config.provider
|
|
1676
2174
|
});
|
|
1677
2175
|
}
|
|
1678
2176
|
const gatewayConfig = {
|
|
@@ -1685,33 +2183,37 @@ function createIssuerService(config) {
|
|
|
1685
2183
|
gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
|
|
1686
2184
|
}
|
|
1687
2185
|
const gateway = new MintingGateway(gatewayConfig);
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2186
|
+
const indexers = /* @__PURE__ */ new Map();
|
|
2187
|
+
for (const tokenAddress of tokenAddresses) {
|
|
2188
|
+
const indexerConfig = {
|
|
2189
|
+
provider: config.provider,
|
|
2190
|
+
pointTokenAddress: tokenAddress,
|
|
2191
|
+
ledger
|
|
2192
|
+
};
|
|
2193
|
+
if (config.indexer?.fromBlock !== void 0) {
|
|
2194
|
+
indexerConfig.fromBlock = config.indexer.fromBlock;
|
|
2195
|
+
}
|
|
2196
|
+
if (config.indexer?.cursorStore) {
|
|
2197
|
+
indexerConfig.cursorStore = config.indexer.cursorStore;
|
|
2198
|
+
}
|
|
2199
|
+
if (config.indexer?.confirmations !== void 0) {
|
|
2200
|
+
indexerConfig.confirmations = config.indexer.confirmations;
|
|
2201
|
+
}
|
|
2202
|
+
if (config.indexer?.batchSize !== void 0) {
|
|
2203
|
+
indexerConfig.batchSize = config.indexer.batchSize;
|
|
2204
|
+
}
|
|
2205
|
+
if (config.indexer?.pollIntervalMs !== void 0) {
|
|
2206
|
+
indexerConfig.pollIntervalMs = config.indexer.pollIntervalMs;
|
|
2207
|
+
}
|
|
2208
|
+
indexers.set(tokenAddress, new PointIndexer(indexerConfig));
|
|
1707
2209
|
}
|
|
1708
|
-
const
|
|
2210
|
+
const firstIndexer = indexers.get(tokenAddresses[0]);
|
|
1709
2211
|
const handlersConfig = {
|
|
1710
2212
|
authService,
|
|
1711
2213
|
gateway,
|
|
1712
2214
|
ledger,
|
|
1713
2215
|
provider: config.provider,
|
|
1714
|
-
|
|
2216
|
+
pointTokenAddresses: tokenAddresses,
|
|
1715
2217
|
chainId: config.chainId,
|
|
1716
2218
|
contracts: config.contracts
|
|
1717
2219
|
};
|
|
@@ -1719,7 +2221,9 @@ function createIssuerService(config) {
|
|
|
1719
2221
|
if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
|
|
1720
2222
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
1721
2223
|
if (config.indexer?.autoStart) {
|
|
1722
|
-
|
|
2224
|
+
for (const idx of indexers.values()) {
|
|
2225
|
+
idx.start();
|
|
2226
|
+
}
|
|
1723
2227
|
}
|
|
1724
2228
|
return {
|
|
1725
2229
|
authService,
|
|
@@ -1730,7 +2234,8 @@ function createIssuerService(config) {
|
|
|
1730
2234
|
relayService,
|
|
1731
2235
|
feeManager,
|
|
1732
2236
|
gateway,
|
|
1733
|
-
|
|
2237
|
+
indexers,
|
|
2238
|
+
indexer: firstIndexer,
|
|
1734
2239
|
handlers
|
|
1735
2240
|
};
|
|
1736
2241
|
}
|
|
@@ -1741,6 +2246,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1741
2246
|
0 && (module.exports = {
|
|
1742
2247
|
AuthError,
|
|
1743
2248
|
AuthService,
|
|
2249
|
+
BalanceAggregator,
|
|
2250
|
+
BurnIndexer,
|
|
1744
2251
|
DefaultPolicyEngine,
|
|
1745
2252
|
FeeManager,
|
|
1746
2253
|
InMemoryCursorStore,
|
|
@@ -1751,6 +2258,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1751
2258
|
MintingGatewayError,
|
|
1752
2259
|
NonceManager,
|
|
1753
2260
|
PAFI_ISSUER_SDK_VERSION,
|
|
2261
|
+
PafiBackendClient,
|
|
2262
|
+
PafiBackendError,
|
|
1754
2263
|
PointIndexer,
|
|
1755
2264
|
PrivateKeySigner,
|
|
1756
2265
|
RelayError,
|