@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.js
CHANGED
|
@@ -11,19 +11,21 @@ var MemoryPointLedger = class {
|
|
|
11
11
|
// -------------------------------------------------------------------------
|
|
12
12
|
// Read
|
|
13
13
|
// -------------------------------------------------------------------------
|
|
14
|
-
async getBalance(userAddress) {
|
|
15
|
-
const
|
|
14
|
+
async getBalance(userAddress, tokenAddress) {
|
|
15
|
+
const user = getAddress(userAddress);
|
|
16
|
+
const token = normalizeToken(tokenAddress);
|
|
16
17
|
this.purgeExpired();
|
|
17
|
-
const total = this.balances.get(
|
|
18
|
-
const locked = this.lockedTotalFor(
|
|
18
|
+
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
19
|
+
const locked = this.lockedTotalFor(user, token);
|
|
19
20
|
return total - locked;
|
|
20
21
|
}
|
|
21
|
-
async getLockedRequests(userAddress) {
|
|
22
|
-
const
|
|
22
|
+
async getLockedRequests(userAddress, tokenAddress) {
|
|
23
|
+
const user = getAddress(userAddress);
|
|
24
|
+
const token = normalizeToken(tokenAddress);
|
|
23
25
|
this.purgeExpired();
|
|
24
26
|
const out = [];
|
|
25
27
|
for (const lock of this.locks.values()) {
|
|
26
|
-
if (lock.userAddress ===
|
|
28
|
+
if (lock.userAddress === user && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
27
29
|
out.push({ ...lock });
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -32,25 +34,28 @@ var MemoryPointLedger = class {
|
|
|
32
34
|
// -------------------------------------------------------------------------
|
|
33
35
|
// Write
|
|
34
36
|
// -------------------------------------------------------------------------
|
|
35
|
-
async creditBalance(userAddress, amount, _reason) {
|
|
37
|
+
async creditBalance(userAddress, amount, _reason, tokenAddress) {
|
|
36
38
|
if (amount <= 0n) {
|
|
37
39
|
throw new Error("MemoryPointLedger: credit amount must be positive");
|
|
38
40
|
}
|
|
39
|
-
const
|
|
41
|
+
const user = getAddress(userAddress);
|
|
42
|
+
const token = normalizeToken(tokenAddress);
|
|
43
|
+
const key = balanceKey(user, token);
|
|
40
44
|
const current = this.balances.get(key) ?? 0n;
|
|
41
45
|
this.balances.set(key, current + amount);
|
|
42
46
|
}
|
|
43
|
-
async lockForMinting(userAddress, amount, lockDurationMs) {
|
|
47
|
+
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
44
48
|
if (amount <= 0n) {
|
|
45
49
|
throw new Error("MemoryPointLedger: lock amount must be positive");
|
|
46
50
|
}
|
|
47
51
|
if (lockDurationMs <= 0) {
|
|
48
52
|
throw new Error("MemoryPointLedger: lockDurationMs must be positive");
|
|
49
53
|
}
|
|
50
|
-
const
|
|
54
|
+
const user = getAddress(userAddress);
|
|
55
|
+
const token = normalizeToken(tokenAddress);
|
|
51
56
|
this.purgeExpired();
|
|
52
|
-
const total = this.balances.get(
|
|
53
|
-
const alreadyLocked = this.lockedTotalFor(
|
|
57
|
+
const total = this.balances.get(balanceKey(user, token)) ?? 0n;
|
|
58
|
+
const alreadyLocked = this.lockedTotalFor(user, token);
|
|
54
59
|
const available = total - alreadyLocked;
|
|
55
60
|
if (available < amount) {
|
|
56
61
|
throw new Error(
|
|
@@ -59,14 +64,18 @@ var MemoryPointLedger = class {
|
|
|
59
64
|
}
|
|
60
65
|
const lockId = `lock-${this.nextLockId++}`;
|
|
61
66
|
const now = this.now();
|
|
62
|
-
|
|
67
|
+
const lock = {
|
|
63
68
|
lockId,
|
|
64
|
-
userAddress:
|
|
69
|
+
userAddress: user,
|
|
65
70
|
amount,
|
|
66
71
|
status: "PENDING",
|
|
67
72
|
createdAt: now,
|
|
68
73
|
expiresAt: now + lockDurationMs
|
|
69
|
-
}
|
|
74
|
+
};
|
|
75
|
+
if (tokenAddress !== void 0) {
|
|
76
|
+
lock.tokenAddress = getAddress(tokenAddress);
|
|
77
|
+
}
|
|
78
|
+
this.locks.set(lockId, lock);
|
|
70
79
|
return lockId;
|
|
71
80
|
}
|
|
72
81
|
async releaseLock(lockId) {
|
|
@@ -76,11 +85,13 @@ var MemoryPointLedger = class {
|
|
|
76
85
|
this.locks.delete(lockId);
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
|
-
async deductBalance(userAddress, amount, txHash) {
|
|
88
|
+
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
80
89
|
if (amount <= 0n) {
|
|
81
90
|
throw new Error("MemoryPointLedger: deduct amount must be positive");
|
|
82
91
|
}
|
|
83
|
-
const
|
|
92
|
+
const user = getAddress(userAddress);
|
|
93
|
+
const token = normalizeToken(tokenAddress);
|
|
94
|
+
const key = balanceKey(user, token);
|
|
84
95
|
const current = this.balances.get(key) ?? 0n;
|
|
85
96
|
if (current < amount) {
|
|
86
97
|
throw new Error(
|
|
@@ -89,7 +100,7 @@ var MemoryPointLedger = class {
|
|
|
89
100
|
}
|
|
90
101
|
this.balances.set(key, current - amount);
|
|
91
102
|
for (const lock of this.locks.values()) {
|
|
92
|
-
if (lock.userAddress ===
|
|
103
|
+
if (lock.userAddress === user && lock.status === "PENDING" && lock.amount === amount && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === token) {
|
|
93
104
|
lock.status = "MINTED";
|
|
94
105
|
lock.txHash = txHash;
|
|
95
106
|
return;
|
|
@@ -105,6 +116,54 @@ var MemoryPointLedger = class {
|
|
|
105
116
|
if (txHash) lock.txHash = txHash;
|
|
106
117
|
}
|
|
107
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
|
+
// -------------------------------------------------------------------------
|
|
108
167
|
// Internal helpers
|
|
109
168
|
// -------------------------------------------------------------------------
|
|
110
169
|
/**
|
|
@@ -119,16 +178,23 @@ var MemoryPointLedger = class {
|
|
|
119
178
|
}
|
|
120
179
|
}
|
|
121
180
|
}
|
|
122
|
-
lockedTotalFor(userAddress) {
|
|
181
|
+
lockedTotalFor(userAddress, tokenKey) {
|
|
123
182
|
let total = 0n;
|
|
124
183
|
for (const lock of this.locks.values()) {
|
|
125
|
-
if (lock.userAddress === userAddress && lock.status === "PENDING") {
|
|
184
|
+
if (lock.userAddress === userAddress && lock.status === "PENDING" && (lock.tokenAddress ?? DEFAULT_TOKEN_KEY) === tokenKey) {
|
|
126
185
|
total += lock.amount;
|
|
127
186
|
}
|
|
128
187
|
}
|
|
129
188
|
return total;
|
|
130
189
|
}
|
|
131
190
|
};
|
|
191
|
+
var DEFAULT_TOKEN_KEY = "default";
|
|
192
|
+
function normalizeToken(tokenAddress) {
|
|
193
|
+
return tokenAddress === void 0 ? DEFAULT_TOKEN_KEY : getAddress(tokenAddress);
|
|
194
|
+
}
|
|
195
|
+
function balanceKey(user, tokenKey) {
|
|
196
|
+
return `${user}|${tokenKey}`;
|
|
197
|
+
}
|
|
132
198
|
|
|
133
199
|
// src/policy/defaultPolicy.ts
|
|
134
200
|
var DefaultPolicyEngine = class {
|
|
@@ -150,7 +216,10 @@ var DefaultPolicyEngine = class {
|
|
|
150
216
|
if (request.amount <= 0n) {
|
|
151
217
|
return { approved: false, reason: "Amount must be positive" };
|
|
152
218
|
}
|
|
153
|
-
const available = await this.ledger.getBalance(
|
|
219
|
+
const available = await this.ledger.getBalance(
|
|
220
|
+
request.userAddress,
|
|
221
|
+
request.pointTokenAddress
|
|
222
|
+
);
|
|
154
223
|
if (available < request.amount) {
|
|
155
224
|
return {
|
|
156
225
|
approved: false,
|
|
@@ -570,6 +639,11 @@ var RelayService = class {
|
|
|
570
639
|
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
571
640
|
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
572
641
|
* need manual review because the tx may still land).
|
|
642
|
+
*
|
|
643
|
+
* @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
|
|
644
|
+
* `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
|
|
645
|
+
* still needs to finalize Relayer v2 ABI before the replacements
|
|
646
|
+
* can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
|
|
573
647
|
*/
|
|
574
648
|
async submitMintAndSwap(params) {
|
|
575
649
|
if (this.simulateBeforeSubmit && this.provider) {
|
|
@@ -648,84 +722,35 @@ var DEFAULT_GAS_UNITS = 500000n;
|
|
|
648
722
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
649
723
|
var FeeManager = class {
|
|
650
724
|
provider;
|
|
651
|
-
|
|
652
|
-
mintAndSwapGasUnits;
|
|
725
|
+
gasUnits;
|
|
653
726
|
gasPremiumBps;
|
|
654
|
-
|
|
655
|
-
rebalanceThresholdWei;
|
|
656
|
-
rebalanceUsdtAmount;
|
|
657
|
-
swapUsdtToNative;
|
|
727
|
+
quoteNativeToFee;
|
|
658
728
|
constructor(config) {
|
|
659
729
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
660
|
-
if (!config.
|
|
661
|
-
throw new Error("FeeManager:
|
|
662
|
-
if (!config.quoteNativeToUsdt)
|
|
663
|
-
throw new Error("FeeManager: quoteNativeToUsdt required");
|
|
730
|
+
if (!config.quoteNativeToFee)
|
|
731
|
+
throw new Error("FeeManager: quoteNativeToFee required");
|
|
664
732
|
this.provider = config.provider;
|
|
665
|
-
this.
|
|
666
|
-
this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
|
|
733
|
+
this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
|
|
667
734
|
this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
|
|
668
|
-
this.
|
|
669
|
-
if (config.rebalanceThresholdWei !== void 0) {
|
|
670
|
-
this.rebalanceThresholdWei = config.rebalanceThresholdWei;
|
|
671
|
-
}
|
|
672
|
-
if (config.rebalanceUsdtAmount !== void 0) {
|
|
673
|
-
this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
|
|
674
|
-
}
|
|
675
|
-
if (config.swapUsdtToNative) {
|
|
676
|
-
this.swapUsdtToNative = config.swapUsdtToNative;
|
|
677
|
-
}
|
|
678
|
-
const rebalanceFields = [
|
|
679
|
-
config.rebalanceThresholdWei,
|
|
680
|
-
config.rebalanceUsdtAmount,
|
|
681
|
-
config.swapUsdtToNative
|
|
682
|
-
];
|
|
683
|
-
const someSet = rebalanceFields.some((v) => v !== void 0);
|
|
684
|
-
const allSet = rebalanceFields.every((v) => v !== void 0);
|
|
685
|
-
if (someSet && !allSet) {
|
|
686
|
-
throw new Error(
|
|
687
|
-
"FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
|
|
688
|
-
);
|
|
689
|
-
}
|
|
735
|
+
this.quoteNativeToFee = config.quoteNativeToFee;
|
|
690
736
|
}
|
|
691
737
|
/**
|
|
692
|
-
* Estimate the
|
|
693
|
-
*
|
|
738
|
+
* Estimate the fee (in the caller's fee currency) to charge for the
|
|
739
|
+
* next sponsored UserOp:
|
|
740
|
+
*
|
|
741
|
+
* nativeCost = gasUnits × gasPrice
|
|
742
|
+
* withPremium = nativeCost × premiumBps / 10_000
|
|
743
|
+
* fee = quoteNativeToFee(withPremium)
|
|
694
744
|
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
745
|
+
* For backward compatibility with v0.2.x code that reads `gasFeeUsdt`
|
|
746
|
+
* from the response, the name `estimateGasFee` is kept — but the
|
|
747
|
+
* currency depends on how the caller wired `quoteNativeToFee`.
|
|
698
748
|
*/
|
|
699
749
|
async estimateGasFee() {
|
|
700
750
|
const gasPrice = await this.provider.getGasPrice();
|
|
701
|
-
const nativeCost = gasPrice * this.
|
|
751
|
+
const nativeCost = gasPrice * this.gasUnits;
|
|
702
752
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
703
|
-
return this.
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Check the operator's native balance and, if it has dropped below the
|
|
707
|
-
* configured threshold, trigger a USDT→native rebalance via the injected
|
|
708
|
-
* `swapUsdtToNative` function.
|
|
709
|
-
*
|
|
710
|
-
* Returns `true` if a rebalance was performed, `false` otherwise.
|
|
711
|
-
* Silently no-ops when rebalance is not configured.
|
|
712
|
-
*/
|
|
713
|
-
async rebalanceIfNeeded() {
|
|
714
|
-
if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
const operatorAddress = this.operatorWallet.account?.address;
|
|
718
|
-
if (!operatorAddress) {
|
|
719
|
-
throw new Error(
|
|
720
|
-
"FeeManager: operator wallet has no account attached \u2014 cannot read balance"
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
const balance = await this.provider.getBalance({ address: operatorAddress });
|
|
724
|
-
if (balance >= this.rebalanceThresholdWei) {
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
await this.swapUsdtToNative(this.rebalanceUsdtAmount);
|
|
728
|
-
return true;
|
|
753
|
+
return this.quoteNativeToFee(withPremium);
|
|
729
754
|
}
|
|
730
755
|
};
|
|
731
756
|
|
|
@@ -774,6 +799,12 @@ var MintingGateway = class {
|
|
|
774
799
|
this.now = config.now ?? (() => Date.now());
|
|
775
800
|
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
776
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* @deprecated Since 0.3.0 — will be renamed to `processMint()` once
|
|
804
|
+
* the SC team finalizes Relayer v2 ABI. The new flow drops the
|
|
805
|
+
* swap steps entirely (no more single-call mint+swap); users swap
|
|
806
|
+
* separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
|
|
807
|
+
*/
|
|
777
808
|
async processMintAndCashOut(request) {
|
|
778
809
|
const { receiverConsent, receiverSignature } = request;
|
|
779
810
|
if (!receiverConsent || !receiverSignature) {
|
|
@@ -838,7 +869,8 @@ var MintingGateway = class {
|
|
|
838
869
|
lockId = await this.ledger.lockForMinting(
|
|
839
870
|
request.userAddress,
|
|
840
871
|
receiverConsent.amount,
|
|
841
|
-
lockDurationMs
|
|
872
|
+
lockDurationMs,
|
|
873
|
+
request.pointTokenAddress
|
|
842
874
|
);
|
|
843
875
|
} catch (err) {
|
|
844
876
|
throw new MintingGatewayError(
|
|
@@ -1128,11 +1160,19 @@ var PointIndexer = class {
|
|
|
1128
1160
|
* issuer to mint without going through the gateway.
|
|
1129
1161
|
*/
|
|
1130
1162
|
async finalize(evt) {
|
|
1131
|
-
const locks = await this.ledger.getLockedRequests(
|
|
1163
|
+
const locks = await this.ledger.getLockedRequests(
|
|
1164
|
+
evt.to,
|
|
1165
|
+
this.pointTokenAddress
|
|
1166
|
+
);
|
|
1132
1167
|
const match = pickMatchingLock(locks, evt.amount);
|
|
1133
1168
|
if (!match) return;
|
|
1134
1169
|
try {
|
|
1135
|
-
await this.ledger.deductBalance(
|
|
1170
|
+
await this.ledger.deductBalance(
|
|
1171
|
+
evt.to,
|
|
1172
|
+
evt.amount,
|
|
1173
|
+
evt.txHash,
|
|
1174
|
+
this.pointTokenAddress
|
|
1175
|
+
);
|
|
1136
1176
|
} catch {
|
|
1137
1177
|
return;
|
|
1138
1178
|
}
|
|
@@ -1154,8 +1194,159 @@ function pickMatchingLock(locks, amount) {
|
|
|
1154
1194
|
return best;
|
|
1155
1195
|
}
|
|
1156
1196
|
|
|
1197
|
+
// src/indexer/burnIndexer.ts
|
|
1198
|
+
import { getAddress as getAddress5, parseAbiItem as parseAbiItem2 } from "viem";
|
|
1199
|
+
var TRANSFER_EVENT2 = parseAbiItem2(
|
|
1200
|
+
"event Transfer(address indexed from, address indexed to, uint256 value)"
|
|
1201
|
+
);
|
|
1202
|
+
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
1203
|
+
var DEFAULT_CONFIRMATIONS2 = 3;
|
|
1204
|
+
var DEFAULT_BATCH_SIZE2 = 2000n;
|
|
1205
|
+
var DEFAULT_POLL_INTERVAL_MS2 = 5e3;
|
|
1206
|
+
var BurnIndexer = class {
|
|
1207
|
+
provider;
|
|
1208
|
+
pointTokenAddress;
|
|
1209
|
+
ledger;
|
|
1210
|
+
cursorStore;
|
|
1211
|
+
startBlock;
|
|
1212
|
+
confirmations;
|
|
1213
|
+
batchSize;
|
|
1214
|
+
pollIntervalMs;
|
|
1215
|
+
/**
|
|
1216
|
+
* Caller-supplied matcher. Return the lockId to resolve for a given
|
|
1217
|
+
* burn event, or `undefined` to skip. Runs synchronously via the
|
|
1218
|
+
* ledger's query path.
|
|
1219
|
+
*
|
|
1220
|
+
* Default: try `ledger.resolveCreditByBurnTx` keyed on a synthetic
|
|
1221
|
+
* lock id `burn-${from}-${amount}` — the in-memory ledger assigns
|
|
1222
|
+
* incrementing IDs so callers with the memory ledger must provide a
|
|
1223
|
+
* custom matcher. Real DB-backed ledgers override this to JOIN on
|
|
1224
|
+
* their `pending_credits` table.
|
|
1225
|
+
*/
|
|
1226
|
+
matchLockId = async () => void 0;
|
|
1227
|
+
running = false;
|
|
1228
|
+
timer;
|
|
1229
|
+
constructor(config) {
|
|
1230
|
+
if (!config.provider) throw new Error("BurnIndexer: provider required");
|
|
1231
|
+
if (!config.pointTokenAddress)
|
|
1232
|
+
throw new Error("BurnIndexer: pointTokenAddress required");
|
|
1233
|
+
if (!config.ledger) throw new Error("BurnIndexer: ledger required");
|
|
1234
|
+
this.provider = config.provider;
|
|
1235
|
+
this.pointTokenAddress = config.pointTokenAddress;
|
|
1236
|
+
this.ledger = config.ledger;
|
|
1237
|
+
this.cursorStore = config.cursorStore ?? new InMemoryCursorStore();
|
|
1238
|
+
this.startBlock = config.fromBlock ?? 0n;
|
|
1239
|
+
this.confirmations = BigInt(
|
|
1240
|
+
config.confirmations ?? DEFAULT_CONFIRMATIONS2
|
|
1241
|
+
);
|
|
1242
|
+
this.batchSize = BigInt(config.batchSize ?? Number(DEFAULT_BATCH_SIZE2));
|
|
1243
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
1244
|
+
}
|
|
1245
|
+
start() {
|
|
1246
|
+
if (this.running) return;
|
|
1247
|
+
this.running = true;
|
|
1248
|
+
void this.tick();
|
|
1249
|
+
}
|
|
1250
|
+
stop() {
|
|
1251
|
+
this.running = false;
|
|
1252
|
+
if (this.timer) {
|
|
1253
|
+
clearTimeout(this.timer);
|
|
1254
|
+
this.timer = void 0;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async tick() {
|
|
1258
|
+
if (!this.running) return;
|
|
1259
|
+
try {
|
|
1260
|
+
const latest = await this.provider.getBlockNumber();
|
|
1261
|
+
const safeHead = latest - this.confirmations;
|
|
1262
|
+
if (safeHead < 0n) {
|
|
1263
|
+
this.scheduleNext();
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const stored = await this.cursorStore.load();
|
|
1267
|
+
const from = stored ?? this.startBlock;
|
|
1268
|
+
if (from > safeHead) {
|
|
1269
|
+
this.scheduleNext();
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
await this.processBlockRange(from, safeHead);
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
this.scheduleNext();
|
|
1276
|
+
}
|
|
1277
|
+
scheduleNext() {
|
|
1278
|
+
if (!this.running) return;
|
|
1279
|
+
this.timer = setTimeout(() => void this.tick(), this.pollIntervalMs);
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Scan `[from, to]` inclusive for burn events. Callers can drive this
|
|
1283
|
+
* directly to backfill a specific range without `start()`. Cursor is
|
|
1284
|
+
* advanced to `to + 1` on completion.
|
|
1285
|
+
*/
|
|
1286
|
+
async processBlockRange(from, to) {
|
|
1287
|
+
if (from > to) return;
|
|
1288
|
+
let cursor = from;
|
|
1289
|
+
while (cursor <= to) {
|
|
1290
|
+
const chunkEnd = cursor + this.batchSize - 1n > to ? to : cursor + this.batchSize - 1n;
|
|
1291
|
+
const logs = await this.provider.getLogs({
|
|
1292
|
+
address: this.pointTokenAddress,
|
|
1293
|
+
event: TRANSFER_EVENT2,
|
|
1294
|
+
args: { to: ZERO_ADDRESS2 },
|
|
1295
|
+
// filter: burn = transfer to zero
|
|
1296
|
+
fromBlock: cursor,
|
|
1297
|
+
toBlock: chunkEnd
|
|
1298
|
+
});
|
|
1299
|
+
const events = this.decodeBurnEvents(logs);
|
|
1300
|
+
events.sort((a, b) => {
|
|
1301
|
+
if (a.blockNumber !== b.blockNumber) {
|
|
1302
|
+
return a.blockNumber < b.blockNumber ? -1 : 1;
|
|
1303
|
+
}
|
|
1304
|
+
return a.logIndex - b.logIndex;
|
|
1305
|
+
});
|
|
1306
|
+
for (const evt of events) {
|
|
1307
|
+
await this.finalize(evt);
|
|
1308
|
+
}
|
|
1309
|
+
await this.cursorStore.save(chunkEnd + 1n);
|
|
1310
|
+
cursor = chunkEnd + 1n;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
decodeBurnEvents(logs) {
|
|
1314
|
+
const out = [];
|
|
1315
|
+
for (const log of logs) {
|
|
1316
|
+
const args = log.args;
|
|
1317
|
+
if (!args.from || !args.to || args.value === void 0) continue;
|
|
1318
|
+
if (getAddress5(args.to) !== ZERO_ADDRESS2) continue;
|
|
1319
|
+
if (log.blockNumber === null || log.transactionHash === null) continue;
|
|
1320
|
+
out.push({
|
|
1321
|
+
from: getAddress5(args.from),
|
|
1322
|
+
amount: args.value,
|
|
1323
|
+
blockNumber: log.blockNumber,
|
|
1324
|
+
txHash: log.transactionHash,
|
|
1325
|
+
logIndex: log.logIndex ?? 0
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
return out;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Resolve a matching pending credit for this burn event and call
|
|
1332
|
+
* `ledger.resolveCreditByBurnTx(lockId, txHash)`. If no match found,
|
|
1333
|
+
* log + skip.
|
|
1334
|
+
*/
|
|
1335
|
+
async finalize(evt) {
|
|
1336
|
+
const lockId = await this.matchLockId(evt);
|
|
1337
|
+
if (!lockId) return;
|
|
1338
|
+
if (!this.ledger.resolveCreditByBurnTx) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
await this.ledger.resolveCreditByBurnTx(lockId, evt.txHash);
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1157
1348
|
// src/api/handlers.ts
|
|
1158
|
-
import { getAddress as
|
|
1349
|
+
import { getAddress as getAddress6 } from "viem";
|
|
1159
1350
|
import {
|
|
1160
1351
|
getMintRequestNonce,
|
|
1161
1352
|
getPointTokenBalance,
|
|
@@ -1169,9 +1360,18 @@ var IssuerApiHandlers = class {
|
|
|
1169
1360
|
gateway;
|
|
1170
1361
|
ledger;
|
|
1171
1362
|
provider;
|
|
1172
|
-
|
|
1363
|
+
/**
|
|
1364
|
+
* Set of supported PointToken addresses (checksum-normalized). Handlers
|
|
1365
|
+
* validate the request's `pointTokenAddress` against this set.
|
|
1366
|
+
*/
|
|
1367
|
+
supportedTokens;
|
|
1368
|
+
/** First supported token — used as default when a handler doesn't
|
|
1369
|
+
* receive a `pointTokenAddress` in the request (shouldn't happen in
|
|
1370
|
+
* practice, but keeps type-narrowing happy). */
|
|
1371
|
+
defaultToken;
|
|
1173
1372
|
chainId;
|
|
1174
1373
|
contracts;
|
|
1374
|
+
pafiWebUrl;
|
|
1175
1375
|
feeManager;
|
|
1176
1376
|
poolsProvider;
|
|
1177
1377
|
constructor(config) {
|
|
@@ -1179,9 +1379,18 @@ var IssuerApiHandlers = class {
|
|
|
1179
1379
|
this.gateway = config.gateway;
|
|
1180
1380
|
this.ledger = config.ledger;
|
|
1181
1381
|
this.provider = config.provider;
|
|
1182
|
-
|
|
1382
|
+
const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
1383
|
+
if (raw.length === 0) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
const normalized = raw.map((a) => getAddress6(a));
|
|
1389
|
+
this.supportedTokens = new Set(normalized);
|
|
1390
|
+
this.defaultToken = normalized[0];
|
|
1183
1391
|
this.chainId = config.chainId;
|
|
1184
1392
|
this.contracts = config.contracts;
|
|
1393
|
+
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1185
1394
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1186
1395
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1187
1396
|
}
|
|
@@ -1217,7 +1426,16 @@ var IssuerApiHandlers = class {
|
|
|
1217
1426
|
`handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
|
|
1218
1427
|
);
|
|
1219
1428
|
}
|
|
1220
|
-
|
|
1429
|
+
const contracts = {
|
|
1430
|
+
...this.contracts,
|
|
1431
|
+
pointTokens: Array.from(this.supportedTokens)
|
|
1432
|
+
};
|
|
1433
|
+
const response = {
|
|
1434
|
+
chainId: this.chainId,
|
|
1435
|
+
contracts
|
|
1436
|
+
};
|
|
1437
|
+
if (this.pafiWebUrl) response.pafiWebUrl = this.pafiWebUrl;
|
|
1438
|
+
return response;
|
|
1221
1439
|
}
|
|
1222
1440
|
/** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
|
|
1223
1441
|
async handleGasFee() {
|
|
@@ -1268,15 +1486,15 @@ var IssuerApiHandlers = class {
|
|
|
1268
1486
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1269
1487
|
);
|
|
1270
1488
|
}
|
|
1271
|
-
const normalizedAuthed =
|
|
1272
|
-
const normalizedRequest =
|
|
1489
|
+
const normalizedAuthed = getAddress6(userAddress);
|
|
1490
|
+
const normalizedRequest = getAddress6(request.userAddress);
|
|
1273
1491
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1274
1492
|
throw new Error(
|
|
1275
1493
|
"handleUser: request userAddress must match authenticated user"
|
|
1276
1494
|
);
|
|
1277
1495
|
}
|
|
1278
|
-
const pointToken =
|
|
1279
|
-
if (
|
|
1496
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1497
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1280
1498
|
throw new Error(
|
|
1281
1499
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
1282
1500
|
);
|
|
@@ -1284,7 +1502,7 @@ var IssuerApiHandlers = class {
|
|
|
1284
1502
|
const [mintRequestNonce, receiverConsentNonce, offChainBalance, onChainBalance, minter] = await Promise.all([
|
|
1285
1503
|
getMintRequestNonce(this.provider, pointToken, normalizedAuthed),
|
|
1286
1504
|
getReceiverConsentNonce(this.provider, pointToken, normalizedAuthed),
|
|
1287
|
-
this.ledger.getBalance(normalizedAuthed),
|
|
1505
|
+
this.ledger.getBalance(normalizedAuthed, pointToken),
|
|
1288
1506
|
getPointTokenBalance(this.provider, pointToken, normalizedAuthed),
|
|
1289
1507
|
isMinter(this.provider, pointToken, normalizedAuthed)
|
|
1290
1508
|
]);
|
|
@@ -1318,8 +1536,8 @@ var IssuerApiHandlers = class {
|
|
|
1318
1536
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1319
1537
|
);
|
|
1320
1538
|
}
|
|
1321
|
-
const pointToken =
|
|
1322
|
-
if (
|
|
1539
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1540
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1323
1541
|
throw new Error(
|
|
1324
1542
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
1325
1543
|
);
|
|
@@ -1343,8 +1561,14 @@ var IssuerApiHandlers = class {
|
|
|
1343
1561
|
/**
|
|
1344
1562
|
* `POST /claim-and-swap`
|
|
1345
1563
|
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1564
|
+
* @deprecated Since 0.3.0 — the single-call mint-then-swap flow is
|
|
1565
|
+
* retired in v1.4. Use the new `handleClaim()` (mint only) and let
|
|
1566
|
+
* the user swap separately on PAFI Web. See
|
|
1567
|
+
* [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
|
|
1568
|
+
* removed in 2.0.
|
|
1569
|
+
*
|
|
1570
|
+
* Legacy behavior: the terminal handler forwards the verified
|
|
1571
|
+
* consent to the MintingGateway, which runs the 11-step flow.
|
|
1348
1572
|
*/
|
|
1349
1573
|
async handleClaimAndSwap(userAddress, request) {
|
|
1350
1574
|
if (request.chainId !== this.chainId) {
|
|
@@ -1352,14 +1576,14 @@ var IssuerApiHandlers = class {
|
|
|
1352
1576
|
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1353
1577
|
);
|
|
1354
1578
|
}
|
|
1355
|
-
const pointToken =
|
|
1356
|
-
if (
|
|
1579
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1580
|
+
if (!this.supportedTokens.has(pointToken)) {
|
|
1357
1581
|
throw new Error(
|
|
1358
1582
|
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1359
1583
|
);
|
|
1360
1584
|
}
|
|
1361
1585
|
const result = await this.gateway.processMintAndCashOut({
|
|
1362
|
-
userAddress:
|
|
1586
|
+
userAddress: getAddress6(userAddress),
|
|
1363
1587
|
pointTokenAddress: pointToken,
|
|
1364
1588
|
chainId: request.chainId,
|
|
1365
1589
|
domain: request.domain,
|
|
@@ -1590,7 +1814,274 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1590
1814
|
return BigInt(whole + padded);
|
|
1591
1815
|
}
|
|
1592
1816
|
|
|
1817
|
+
// src/balance/balanceAggregator.ts
|
|
1818
|
+
import { getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
|
|
1819
|
+
var BalanceAggregator = class {
|
|
1820
|
+
provider;
|
|
1821
|
+
ledger;
|
|
1822
|
+
constructor(config) {
|
|
1823
|
+
if (!config.provider) {
|
|
1824
|
+
throw new Error("BalanceAggregator: provider is required");
|
|
1825
|
+
}
|
|
1826
|
+
if (!config.ledger) {
|
|
1827
|
+
throw new Error("BalanceAggregator: ledger is required");
|
|
1828
|
+
}
|
|
1829
|
+
this.provider = config.provider;
|
|
1830
|
+
this.ledger = config.ledger;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Combined balance for a single (user, token) pair. Fetches off-chain
|
|
1834
|
+
* + on-chain in parallel.
|
|
1835
|
+
*/
|
|
1836
|
+
async getCombinedBalance(user, pointToken) {
|
|
1837
|
+
const [offChain, onChain] = await Promise.all([
|
|
1838
|
+
this.ledger.getBalance(user, pointToken),
|
|
1839
|
+
getPointTokenBalance2(this.provider, pointToken, user)
|
|
1840
|
+
]);
|
|
1841
|
+
return {
|
|
1842
|
+
offChain,
|
|
1843
|
+
onChain,
|
|
1844
|
+
total: offChain + onChain
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Combined balance for multiple tokens owned by the same user. Runs
|
|
1849
|
+
* all lookups in parallel. Returns a Map keyed by the token address
|
|
1850
|
+
* (same casing as supplied — caller should normalize if needed).
|
|
1851
|
+
*/
|
|
1852
|
+
async getCombinedBalanceMulti(user, pointTokens) {
|
|
1853
|
+
const entries = await Promise.all(
|
|
1854
|
+
pointTokens.map(async (token) => {
|
|
1855
|
+
const balance = await this.getCombinedBalance(user, token);
|
|
1856
|
+
return [token, balance];
|
|
1857
|
+
})
|
|
1858
|
+
);
|
|
1859
|
+
return new Map(entries);
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// src/pafi-backend/types.ts
|
|
1864
|
+
var PafiBackendError = class extends Error {
|
|
1865
|
+
constructor(code, message, httpStatus, details, opts) {
|
|
1866
|
+
super(message);
|
|
1867
|
+
this.code = code;
|
|
1868
|
+
this.httpStatus = httpStatus;
|
|
1869
|
+
this.details = details;
|
|
1870
|
+
this.name = "PafiBackendError";
|
|
1871
|
+
if (opts?.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
|
|
1872
|
+
if (opts?.safeToRetry !== void 0) this.serverSafeToRetry = opts.safeToRetry;
|
|
1873
|
+
}
|
|
1874
|
+
code;
|
|
1875
|
+
httpStatus;
|
|
1876
|
+
details;
|
|
1877
|
+
/**
|
|
1878
|
+
* Seconds to wait before retry. Populated from the server body
|
|
1879
|
+
* (e.g. rate limit returns the number of seconds until UTC midnight).
|
|
1880
|
+
*/
|
|
1881
|
+
retryAfter;
|
|
1882
|
+
/**
|
|
1883
|
+
* `safeToRetry` as reported by the server body. Prefer this over the
|
|
1884
|
+
* code-based heuristic when available — the server knows more about
|
|
1885
|
+
* whether the same request will succeed on retry.
|
|
1886
|
+
*/
|
|
1887
|
+
serverSafeToRetry;
|
|
1888
|
+
/**
|
|
1889
|
+
* Whether the caller can safely retry the same request.
|
|
1890
|
+
*
|
|
1891
|
+
* If the server provided `safeToRetry` in the body, trust that.
|
|
1892
|
+
* Otherwise fall back to a code-based heuristic.
|
|
1893
|
+
*/
|
|
1894
|
+
get safeToRetry() {
|
|
1895
|
+
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
1896
|
+
switch (this.code) {
|
|
1897
|
+
case "PAYMASTER_UNAVAILABLE":
|
|
1898
|
+
case "PAYMASTER_TIMEOUT":
|
|
1899
|
+
case "RATE_LIMITER_UNAVAILABLE":
|
|
1900
|
+
case "INTERNAL_ERROR":
|
|
1901
|
+
case "TIMEOUT":
|
|
1902
|
+
case "NETWORK_ERROR":
|
|
1903
|
+
return true;
|
|
1904
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
1905
|
+
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
1906
|
+
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
1907
|
+
return true;
|
|
1908
|
+
// after retryAfter
|
|
1909
|
+
default:
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
// src/pafi-backend/pafiBackendClient.ts
|
|
1916
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1917
|
+
var RETRY_DEFAULTS = {
|
|
1918
|
+
maxAttempts: 1,
|
|
1919
|
+
initialDelayMs: 500,
|
|
1920
|
+
maxDelayMs: 1e4,
|
|
1921
|
+
maxRetryAfterMs: 3e4
|
|
1922
|
+
};
|
|
1923
|
+
var PafiBackendClient = class {
|
|
1924
|
+
url;
|
|
1925
|
+
issuerId;
|
|
1926
|
+
apiKey;
|
|
1927
|
+
fetchImpl;
|
|
1928
|
+
timeoutMs;
|
|
1929
|
+
retry;
|
|
1930
|
+
constructor(config) {
|
|
1931
|
+
if (!config.url) {
|
|
1932
|
+
throw new Error("PafiBackendClient: url is required");
|
|
1933
|
+
}
|
|
1934
|
+
if (!config.issuerId) {
|
|
1935
|
+
throw new Error("PafiBackendClient: issuerId is required");
|
|
1936
|
+
}
|
|
1937
|
+
if (!config.apiKey) {
|
|
1938
|
+
throw new Error("PafiBackendClient: apiKey is required");
|
|
1939
|
+
}
|
|
1940
|
+
this.url = config.url.replace(/\/+$/, "");
|
|
1941
|
+
this.issuerId = config.issuerId;
|
|
1942
|
+
this.apiKey = config.apiKey;
|
|
1943
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
1944
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1945
|
+
this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
|
|
1946
|
+
if (!this.fetchImpl) {
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
"PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
if (this.retry.maxAttempts < 1) {
|
|
1952
|
+
throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Request paymaster sponsorship for a pre-built UserOperation.
|
|
1957
|
+
* See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
|
|
1958
|
+
*
|
|
1959
|
+
* Retries automatically on transient failures (5xx, timeouts, network
|
|
1960
|
+
* errors, and errors the server flags with `safeToRetry: true`) up to
|
|
1961
|
+
* `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
|
|
1962
|
+
*
|
|
1963
|
+
* @throws PafiBackendError on final failure after exhausting retries
|
|
1964
|
+
*/
|
|
1965
|
+
async requestSponsorship(req) {
|
|
1966
|
+
return this.postWithRetry(
|
|
1967
|
+
"/paymaster/sponsor",
|
|
1968
|
+
req
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
// -------------------------------------------------------------------------
|
|
1972
|
+
// Internals
|
|
1973
|
+
// -------------------------------------------------------------------------
|
|
1974
|
+
async postWithRetry(path, body) {
|
|
1975
|
+
let lastError;
|
|
1976
|
+
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
1977
|
+
try {
|
|
1978
|
+
return await this.post(path, body);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
if (!(err instanceof PafiBackendError)) throw err;
|
|
1981
|
+
lastError = err;
|
|
1982
|
+
const isLastAttempt = attempt >= this.retry.maxAttempts;
|
|
1983
|
+
if (isLastAttempt || !err.safeToRetry) throw err;
|
|
1984
|
+
const delay = this.computeBackoff(attempt, err.retryAfter);
|
|
1985
|
+
if (delay === null) throw err;
|
|
1986
|
+
await this.sleep(delay);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
throw lastError;
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Pick the delay before the next retry.
|
|
1993
|
+
* - If the server sent `retryAfter` (seconds), honor it (capped by
|
|
1994
|
+
* `maxRetryAfterMs`) — returns null if the server wait exceeds the
|
|
1995
|
+
* cap, signalling the caller should give up.
|
|
1996
|
+
* - Otherwise: exponential backoff with ±20% jitter, capped at
|
|
1997
|
+
* `maxDelayMs`.
|
|
1998
|
+
*/
|
|
1999
|
+
computeBackoff(attempt, retryAfter) {
|
|
2000
|
+
if (retryAfter !== void 0) {
|
|
2001
|
+
const serverMs = retryAfter * 1e3;
|
|
2002
|
+
if (serverMs > this.retry.maxRetryAfterMs) return null;
|
|
2003
|
+
return serverMs;
|
|
2004
|
+
}
|
|
2005
|
+
const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
|
|
2006
|
+
const capped = Math.min(exp, this.retry.maxDelayMs);
|
|
2007
|
+
const jitter = capped * (0.8 + Math.random() * 0.4);
|
|
2008
|
+
return Math.round(jitter);
|
|
2009
|
+
}
|
|
2010
|
+
sleep(ms) {
|
|
2011
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2012
|
+
}
|
|
2013
|
+
async post(path, body) {
|
|
2014
|
+
const controller = new AbortController();
|
|
2015
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2016
|
+
let response;
|
|
2017
|
+
try {
|
|
2018
|
+
response = await this.fetchImpl(`${this.url}${path}`, {
|
|
2019
|
+
method: "POST",
|
|
2020
|
+
headers: {
|
|
2021
|
+
"Content-Type": "application/json",
|
|
2022
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
2023
|
+
"X-Issuer-Id": this.issuerId
|
|
2024
|
+
},
|
|
2025
|
+
body: JSON.stringify(body, this.bigintReplacer),
|
|
2026
|
+
signal: controller.signal
|
|
2027
|
+
});
|
|
2028
|
+
} catch (err) {
|
|
2029
|
+
if (err.name === "AbortError") {
|
|
2030
|
+
throw new PafiBackendError(
|
|
2031
|
+
"TIMEOUT",
|
|
2032
|
+
`PAFI Backend request timed out after ${this.timeoutMs}ms`,
|
|
2033
|
+
0
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
throw new PafiBackendError(
|
|
2037
|
+
"NETWORK_ERROR",
|
|
2038
|
+
`PAFI Backend unreachable: ${err.message}`,
|
|
2039
|
+
0
|
|
2040
|
+
);
|
|
2041
|
+
} finally {
|
|
2042
|
+
clearTimeout(timeoutId);
|
|
2043
|
+
}
|
|
2044
|
+
const text = await response.text();
|
|
2045
|
+
if (!response.ok) {
|
|
2046
|
+
let code = "INTERNAL_ERROR";
|
|
2047
|
+
let message = text || response.statusText;
|
|
2048
|
+
let details;
|
|
2049
|
+
let retryAfter;
|
|
2050
|
+
let serverSafeToRetry;
|
|
2051
|
+
try {
|
|
2052
|
+
const parsed = JSON.parse(text);
|
|
2053
|
+
code = parsed.code ?? code;
|
|
2054
|
+
message = parsed.message ?? message;
|
|
2055
|
+
details = parsed.details;
|
|
2056
|
+
if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
|
|
2057
|
+
if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
|
|
2058
|
+
} catch {
|
|
2059
|
+
}
|
|
2060
|
+
throw new PafiBackendError(code, message, response.status, details, {
|
|
2061
|
+
...retryAfter !== void 0 ? { retryAfter } : {},
|
|
2062
|
+
...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
return JSON.parse(text, this.bigintReviver);
|
|
2066
|
+
}
|
|
2067
|
+
/** JSON replacer that stringifies bigints. Paired with bigintReviver. */
|
|
2068
|
+
bigintReplacer = (_key, value) => {
|
|
2069
|
+
return typeof value === "bigint" ? value.toString() : value;
|
|
2070
|
+
};
|
|
2071
|
+
/**
|
|
2072
|
+
* JSON reviver that coerces specific numeric-string fields back to
|
|
2073
|
+
* bigint. The server must send these fields as decimal strings.
|
|
2074
|
+
*/
|
|
2075
|
+
bigintReviver = (key, value) => {
|
|
2076
|
+
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)) {
|
|
2077
|
+
return BigInt(value);
|
|
2078
|
+
}
|
|
2079
|
+
return value;
|
|
2080
|
+
};
|
|
2081
|
+
};
|
|
2082
|
+
|
|
1593
2083
|
// src/config.ts
|
|
2084
|
+
import { getAddress as getAddress7 } from "viem";
|
|
1594
2085
|
function createIssuerService(config) {
|
|
1595
2086
|
if (!config.provider) {
|
|
1596
2087
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -1601,9 +2092,6 @@ function createIssuerService(config) {
|
|
|
1601
2092
|
if (!config.signer) {
|
|
1602
2093
|
throw new Error("createIssuerService: signer is required");
|
|
1603
2094
|
}
|
|
1604
|
-
if (!config.pointTokenAddress) {
|
|
1605
|
-
throw new Error("createIssuerService: pointTokenAddress is required");
|
|
1606
|
-
}
|
|
1607
2095
|
if (!config.relayAddress) {
|
|
1608
2096
|
throw new Error("createIssuerService: relayAddress is required");
|
|
1609
2097
|
}
|
|
@@ -1613,6 +2101,13 @@ function createIssuerService(config) {
|
|
|
1613
2101
|
if (!config.auth?.domain) {
|
|
1614
2102
|
throw new Error("createIssuerService: auth.domain is required");
|
|
1615
2103
|
}
|
|
2104
|
+
const rawAddresses = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
2105
|
+
if (rawAddresses.length === 0) {
|
|
2106
|
+
throw new Error(
|
|
2107
|
+
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
const tokenAddresses = rawAddresses.map((a) => getAddress7(a));
|
|
1616
2111
|
const ledger = config.ledger ?? new MemoryPointLedger();
|
|
1617
2112
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1618
2113
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
@@ -1642,8 +2137,7 @@ function createIssuerService(config) {
|
|
|
1642
2137
|
if (config.fee) {
|
|
1643
2138
|
feeManager = new FeeManager({
|
|
1644
2139
|
...config.fee,
|
|
1645
|
-
provider: config.provider
|
|
1646
|
-
operatorWallet: config.operatorWallet
|
|
2140
|
+
provider: config.provider
|
|
1647
2141
|
});
|
|
1648
2142
|
}
|
|
1649
2143
|
const gatewayConfig = {
|
|
@@ -1656,33 +2150,37 @@ function createIssuerService(config) {
|
|
|
1656
2150
|
gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
|
|
1657
2151
|
}
|
|
1658
2152
|
const gateway = new MintingGateway(gatewayConfig);
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2153
|
+
const indexers = /* @__PURE__ */ new Map();
|
|
2154
|
+
for (const tokenAddress of tokenAddresses) {
|
|
2155
|
+
const indexerConfig = {
|
|
2156
|
+
provider: config.provider,
|
|
2157
|
+
pointTokenAddress: tokenAddress,
|
|
2158
|
+
ledger
|
|
2159
|
+
};
|
|
2160
|
+
if (config.indexer?.fromBlock !== void 0) {
|
|
2161
|
+
indexerConfig.fromBlock = config.indexer.fromBlock;
|
|
2162
|
+
}
|
|
2163
|
+
if (config.indexer?.cursorStore) {
|
|
2164
|
+
indexerConfig.cursorStore = config.indexer.cursorStore;
|
|
2165
|
+
}
|
|
2166
|
+
if (config.indexer?.confirmations !== void 0) {
|
|
2167
|
+
indexerConfig.confirmations = config.indexer.confirmations;
|
|
2168
|
+
}
|
|
2169
|
+
if (config.indexer?.batchSize !== void 0) {
|
|
2170
|
+
indexerConfig.batchSize = config.indexer.batchSize;
|
|
2171
|
+
}
|
|
2172
|
+
if (config.indexer?.pollIntervalMs !== void 0) {
|
|
2173
|
+
indexerConfig.pollIntervalMs = config.indexer.pollIntervalMs;
|
|
2174
|
+
}
|
|
2175
|
+
indexers.set(tokenAddress, new PointIndexer(indexerConfig));
|
|
1678
2176
|
}
|
|
1679
|
-
const
|
|
2177
|
+
const firstIndexer = indexers.get(tokenAddresses[0]);
|
|
1680
2178
|
const handlersConfig = {
|
|
1681
2179
|
authService,
|
|
1682
2180
|
gateway,
|
|
1683
2181
|
ledger,
|
|
1684
2182
|
provider: config.provider,
|
|
1685
|
-
|
|
2183
|
+
pointTokenAddresses: tokenAddresses,
|
|
1686
2184
|
chainId: config.chainId,
|
|
1687
2185
|
contracts: config.contracts
|
|
1688
2186
|
};
|
|
@@ -1690,7 +2188,9 @@ function createIssuerService(config) {
|
|
|
1690
2188
|
if (config.poolsProvider) handlersConfig.poolsProvider = config.poolsProvider;
|
|
1691
2189
|
const handlers = new IssuerApiHandlers(handlersConfig);
|
|
1692
2190
|
if (config.indexer?.autoStart) {
|
|
1693
|
-
|
|
2191
|
+
for (const idx of indexers.values()) {
|
|
2192
|
+
idx.start();
|
|
2193
|
+
}
|
|
1694
2194
|
}
|
|
1695
2195
|
return {
|
|
1696
2196
|
authService,
|
|
@@ -1701,7 +2201,8 @@ function createIssuerService(config) {
|
|
|
1701
2201
|
relayService,
|
|
1702
2202
|
feeManager,
|
|
1703
2203
|
gateway,
|
|
1704
|
-
|
|
2204
|
+
indexers,
|
|
2205
|
+
indexer: firstIndexer,
|
|
1705
2206
|
handlers
|
|
1706
2207
|
};
|
|
1707
2208
|
}
|
|
@@ -1711,6 +2212,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1711
2212
|
export {
|
|
1712
2213
|
AuthError,
|
|
1713
2214
|
AuthService,
|
|
2215
|
+
BalanceAggregator,
|
|
2216
|
+
BurnIndexer,
|
|
1714
2217
|
DefaultPolicyEngine,
|
|
1715
2218
|
FeeManager,
|
|
1716
2219
|
InMemoryCursorStore,
|
|
@@ -1721,6 +2224,8 @@ export {
|
|
|
1721
2224
|
MintingGatewayError,
|
|
1722
2225
|
NonceManager,
|
|
1723
2226
|
PAFI_ISSUER_SDK_VERSION,
|
|
2227
|
+
PafiBackendClient,
|
|
2228
|
+
PafiBackendError,
|
|
1724
2229
|
PointIndexer,
|
|
1725
2230
|
PrivateKeySigner,
|
|
1726
2231
|
RelayError,
|