@pafi-dev/issuer 0.2.0 → 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 +532 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +418 -79
- package/dist/index.d.ts +418 -79
- package/dist/index.js +528 -81
- 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,
|
|
@@ -162,6 +166,54 @@ var MemoryPointLedger = class {
|
|
|
162
166
|
if (txHash) lock.txHash = txHash;
|
|
163
167
|
}
|
|
164
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
|
+
// -------------------------------------------------------------------------
|
|
165
217
|
// Internal helpers
|
|
166
218
|
// -------------------------------------------------------------------------
|
|
167
219
|
/**
|
|
@@ -630,6 +682,11 @@ var RelayService = class {
|
|
|
630
682
|
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
631
683
|
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
632
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.
|
|
633
690
|
*/
|
|
634
691
|
async submitMintAndSwap(params) {
|
|
635
692
|
if (this.simulateBeforeSubmit && this.provider) {
|
|
@@ -708,84 +765,35 @@ var DEFAULT_GAS_UNITS = 500000n;
|
|
|
708
765
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
709
766
|
var FeeManager = class {
|
|
710
767
|
provider;
|
|
711
|
-
|
|
712
|
-
mintAndSwapGasUnits;
|
|
768
|
+
gasUnits;
|
|
713
769
|
gasPremiumBps;
|
|
714
|
-
|
|
715
|
-
rebalanceThresholdWei;
|
|
716
|
-
rebalanceUsdtAmount;
|
|
717
|
-
swapUsdtToNative;
|
|
770
|
+
quoteNativeToFee;
|
|
718
771
|
constructor(config) {
|
|
719
772
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
720
|
-
if (!config.
|
|
721
|
-
throw new Error("FeeManager:
|
|
722
|
-
if (!config.quoteNativeToUsdt)
|
|
723
|
-
throw new Error("FeeManager: quoteNativeToUsdt required");
|
|
773
|
+
if (!config.quoteNativeToFee)
|
|
774
|
+
throw new Error("FeeManager: quoteNativeToFee required");
|
|
724
775
|
this.provider = config.provider;
|
|
725
|
-
this.
|
|
726
|
-
this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
|
|
776
|
+
this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
|
|
727
777
|
this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
|
|
728
|
-
this.
|
|
729
|
-
if (config.rebalanceThresholdWei !== void 0) {
|
|
730
|
-
this.rebalanceThresholdWei = config.rebalanceThresholdWei;
|
|
731
|
-
}
|
|
732
|
-
if (config.rebalanceUsdtAmount !== void 0) {
|
|
733
|
-
this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
|
|
734
|
-
}
|
|
735
|
-
if (config.swapUsdtToNative) {
|
|
736
|
-
this.swapUsdtToNative = config.swapUsdtToNative;
|
|
737
|
-
}
|
|
738
|
-
const rebalanceFields = [
|
|
739
|
-
config.rebalanceThresholdWei,
|
|
740
|
-
config.rebalanceUsdtAmount,
|
|
741
|
-
config.swapUsdtToNative
|
|
742
|
-
];
|
|
743
|
-
const someSet = rebalanceFields.some((v) => v !== void 0);
|
|
744
|
-
const allSet = rebalanceFields.every((v) => v !== void 0);
|
|
745
|
-
if (someSet && !allSet) {
|
|
746
|
-
throw new Error(
|
|
747
|
-
"FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
|
|
748
|
-
);
|
|
749
|
-
}
|
|
778
|
+
this.quoteNativeToFee = config.quoteNativeToFee;
|
|
750
779
|
}
|
|
751
780
|
/**
|
|
752
|
-
* Estimate the
|
|
753
|
-
*
|
|
781
|
+
* Estimate the fee (in the caller's fee currency) to charge for the
|
|
782
|
+
* next sponsored UserOp:
|
|
754
783
|
*
|
|
755
|
-
* nativeCost
|
|
756
|
-
*
|
|
757
|
-
*
|
|
784
|
+
* nativeCost = gasUnits × gasPrice
|
|
785
|
+
* withPremium = nativeCost × premiumBps / 10_000
|
|
786
|
+
* fee = quoteNativeToFee(withPremium)
|
|
787
|
+
*
|
|
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`.
|
|
758
791
|
*/
|
|
759
792
|
async estimateGasFee() {
|
|
760
793
|
const gasPrice = await this.provider.getGasPrice();
|
|
761
|
-
const nativeCost = gasPrice * this.
|
|
794
|
+
const nativeCost = gasPrice * this.gasUnits;
|
|
762
795
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
763
|
-
return this.
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Check the operator's native balance and, if it has dropped below the
|
|
767
|
-
* configured threshold, trigger a USDT→native rebalance via the injected
|
|
768
|
-
* `swapUsdtToNative` function.
|
|
769
|
-
*
|
|
770
|
-
* Returns `true` if a rebalance was performed, `false` otherwise.
|
|
771
|
-
* Silently no-ops when rebalance is not configured.
|
|
772
|
-
*/
|
|
773
|
-
async rebalanceIfNeeded() {
|
|
774
|
-
if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
const operatorAddress = this.operatorWallet.account?.address;
|
|
778
|
-
if (!operatorAddress) {
|
|
779
|
-
throw new Error(
|
|
780
|
-
"FeeManager: operator wallet has no account attached \u2014 cannot read balance"
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
const balance = await this.provider.getBalance({ address: operatorAddress });
|
|
784
|
-
if (balance >= this.rebalanceThresholdWei) {
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
await this.swapUsdtToNative(this.rebalanceUsdtAmount);
|
|
788
|
-
return true;
|
|
796
|
+
return this.quoteNativeToFee(withPremium);
|
|
789
797
|
}
|
|
790
798
|
};
|
|
791
799
|
|
|
@@ -831,6 +839,12 @@ var MintingGateway = class {
|
|
|
831
839
|
this.now = config.now ?? (() => Date.now());
|
|
832
840
|
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
833
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
|
+
*/
|
|
834
848
|
async processMintAndCashOut(request) {
|
|
835
849
|
const { receiverConsent, receiverSignature } = request;
|
|
836
850
|
if (!receiverConsent || !receiverSignature) {
|
|
@@ -1220,8 +1234,159 @@ function pickMatchingLock(locks, amount) {
|
|
|
1220
1234
|
return best;
|
|
1221
1235
|
}
|
|
1222
1236
|
|
|
1223
|
-
// src/
|
|
1237
|
+
// src/indexer/burnIndexer.ts
|
|
1224
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");
|
|
1225
1390
|
var import_core5 = require("@pafi-dev/core");
|
|
1226
1391
|
var IssuerApiHandlers = class {
|
|
1227
1392
|
authService;
|
|
@@ -1239,6 +1404,7 @@ var IssuerApiHandlers = class {
|
|
|
1239
1404
|
defaultToken;
|
|
1240
1405
|
chainId;
|
|
1241
1406
|
contracts;
|
|
1407
|
+
pafiWebUrl;
|
|
1242
1408
|
feeManager;
|
|
1243
1409
|
poolsProvider;
|
|
1244
1410
|
constructor(config) {
|
|
@@ -1252,11 +1418,12 @@ var IssuerApiHandlers = class {
|
|
|
1252
1418
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1253
1419
|
);
|
|
1254
1420
|
}
|
|
1255
|
-
const normalized = raw.map((a) => (0,
|
|
1421
|
+
const normalized = raw.map((a) => (0, import_viem7.getAddress)(a));
|
|
1256
1422
|
this.supportedTokens = new Set(normalized);
|
|
1257
1423
|
this.defaultToken = normalized[0];
|
|
1258
1424
|
this.chainId = config.chainId;
|
|
1259
1425
|
this.contracts = config.contracts;
|
|
1426
|
+
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1260
1427
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1261
1428
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1262
1429
|
}
|
|
@@ -1292,7 +1459,16 @@ var IssuerApiHandlers = class {
|
|
|
1292
1459
|
`handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
|
|
1293
1460
|
);
|
|
1294
1461
|
}
|
|
1295
|
-
|
|
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;
|
|
1296
1472
|
}
|
|
1297
1473
|
/** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
|
|
1298
1474
|
async handleGasFee() {
|
|
@@ -1343,14 +1519,14 @@ var IssuerApiHandlers = class {
|
|
|
1343
1519
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1344
1520
|
);
|
|
1345
1521
|
}
|
|
1346
|
-
const normalizedAuthed = (0,
|
|
1347
|
-
const normalizedRequest = (0,
|
|
1522
|
+
const normalizedAuthed = (0, import_viem7.getAddress)(userAddress);
|
|
1523
|
+
const normalizedRequest = (0, import_viem7.getAddress)(request.userAddress);
|
|
1348
1524
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1349
1525
|
throw new Error(
|
|
1350
1526
|
"handleUser: request userAddress must match authenticated user"
|
|
1351
1527
|
);
|
|
1352
1528
|
}
|
|
1353
|
-
const pointToken = (0,
|
|
1529
|
+
const pointToken = (0, import_viem7.getAddress)(request.pointTokenAddress);
|
|
1354
1530
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1355
1531
|
throw new Error(
|
|
1356
1532
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
@@ -1393,7 +1569,7 @@ var IssuerApiHandlers = class {
|
|
|
1393
1569
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1394
1570
|
);
|
|
1395
1571
|
}
|
|
1396
|
-
const pointToken = (0,
|
|
1572
|
+
const pointToken = (0, import_viem7.getAddress)(request.pointTokenAddress);
|
|
1397
1573
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1398
1574
|
throw new Error(
|
|
1399
1575
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
@@ -1418,8 +1594,14 @@ var IssuerApiHandlers = class {
|
|
|
1418
1594
|
/**
|
|
1419
1595
|
* `POST /claim-and-swap`
|
|
1420
1596
|
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
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.
|
|
1423
1605
|
*/
|
|
1424
1606
|
async handleClaimAndSwap(userAddress, request) {
|
|
1425
1607
|
if (request.chainId !== this.chainId) {
|
|
@@ -1427,14 +1609,14 @@ var IssuerApiHandlers = class {
|
|
|
1427
1609
|
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1428
1610
|
);
|
|
1429
1611
|
}
|
|
1430
|
-
const pointToken = (0,
|
|
1612
|
+
const pointToken = (0, import_viem7.getAddress)(request.pointTokenAddress);
|
|
1431
1613
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1432
1614
|
throw new Error(
|
|
1433
1615
|
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1434
1616
|
);
|
|
1435
1617
|
}
|
|
1436
1618
|
const result = await this.gateway.processMintAndCashOut({
|
|
1437
|
-
userAddress: (0,
|
|
1619
|
+
userAddress: (0, import_viem7.getAddress)(userAddress),
|
|
1438
1620
|
pointTokenAddress: pointToken,
|
|
1439
1621
|
chainId: request.chainId,
|
|
1440
1622
|
domain: request.domain,
|
|
@@ -1665,8 +1847,274 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1665
1847
|
return BigInt(whole + padded);
|
|
1666
1848
|
}
|
|
1667
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
|
+
|
|
1668
2116
|
// src/config.ts
|
|
1669
|
-
var
|
|
2117
|
+
var import_viem8 = require("viem");
|
|
1670
2118
|
function createIssuerService(config) {
|
|
1671
2119
|
if (!config.provider) {
|
|
1672
2120
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -1692,7 +2140,7 @@ function createIssuerService(config) {
|
|
|
1692
2140
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
1693
2141
|
);
|
|
1694
2142
|
}
|
|
1695
|
-
const tokenAddresses = rawAddresses.map((a) => (0,
|
|
2143
|
+
const tokenAddresses = rawAddresses.map((a) => (0, import_viem8.getAddress)(a));
|
|
1696
2144
|
const ledger = config.ledger ?? new MemoryPointLedger();
|
|
1697
2145
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1698
2146
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
@@ -1722,8 +2170,7 @@ function createIssuerService(config) {
|
|
|
1722
2170
|
if (config.fee) {
|
|
1723
2171
|
feeManager = new FeeManager({
|
|
1724
2172
|
...config.fee,
|
|
1725
|
-
provider: config.provider
|
|
1726
|
-
operatorWallet: config.operatorWallet
|
|
2173
|
+
provider: config.provider
|
|
1727
2174
|
});
|
|
1728
2175
|
}
|
|
1729
2176
|
const gatewayConfig = {
|
|
@@ -1799,6 +2246,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1799
2246
|
0 && (module.exports = {
|
|
1800
2247
|
AuthError,
|
|
1801
2248
|
AuthService,
|
|
2249
|
+
BalanceAggregator,
|
|
2250
|
+
BurnIndexer,
|
|
1802
2251
|
DefaultPolicyEngine,
|
|
1803
2252
|
FeeManager,
|
|
1804
2253
|
InMemoryCursorStore,
|
|
@@ -1809,6 +2258,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1809
2258
|
MintingGatewayError,
|
|
1810
2259
|
NonceManager,
|
|
1811
2260
|
PAFI_ISSUER_SDK_VERSION,
|
|
2261
|
+
PafiBackendClient,
|
|
2262
|
+
PafiBackendError,
|
|
1812
2263
|
PointIndexer,
|
|
1813
2264
|
PrivateKeySigner,
|
|
1814
2265
|
RelayError,
|