@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.js
CHANGED
|
@@ -116,6 +116,54 @@ var MemoryPointLedger = class {
|
|
|
116
116
|
if (txHash) lock.txHash = txHash;
|
|
117
117
|
}
|
|
118
118
|
// -------------------------------------------------------------------------
|
|
119
|
+
// v1.4 — Reverse flow (PT burn → off-chain credit)
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
pendingCredits = /* @__PURE__ */ new Map();
|
|
122
|
+
nextCreditId = 1;
|
|
123
|
+
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
124
|
+
if (amount <= 0n) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"MemoryPointLedger: pending credit amount must be positive"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (durationMs <= 0) {
|
|
130
|
+
throw new Error("MemoryPointLedger: durationMs must be positive");
|
|
131
|
+
}
|
|
132
|
+
const user = getAddress(userAddress);
|
|
133
|
+
const lockId = `credit-${this.nextCreditId++}`;
|
|
134
|
+
const now = this.now();
|
|
135
|
+
this.pendingCredits.set(lockId, {
|
|
136
|
+
lockId,
|
|
137
|
+
userAddress: user,
|
|
138
|
+
amount,
|
|
139
|
+
tokenAddress: tokenAddress !== void 0 ? getAddress(tokenAddress) : void 0,
|
|
140
|
+
createdAt: now,
|
|
141
|
+
expiresAt: now + durationMs,
|
|
142
|
+
status: "PENDING"
|
|
143
|
+
});
|
|
144
|
+
return lockId;
|
|
145
|
+
}
|
|
146
|
+
async resolveCreditByBurnTx(lockId, txHash) {
|
|
147
|
+
const credit = this.pendingCredits.get(lockId);
|
|
148
|
+
if (!credit) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`MemoryPointLedger: unknown pending credit lockId ${lockId}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (credit.status === "RESOLVED") {
|
|
154
|
+
if (credit.txHash === txHash) return;
|
|
155
|
+
throw new Error(
|
|
156
|
+
`MemoryPointLedger: credit ${lockId} already resolved with a different txHash`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const token = normalizeToken(credit.tokenAddress);
|
|
160
|
+
const key = balanceKey(credit.userAddress, token);
|
|
161
|
+
const current = this.balances.get(key) ?? 0n;
|
|
162
|
+
this.balances.set(key, current + credit.amount);
|
|
163
|
+
credit.status = "RESOLVED";
|
|
164
|
+
credit.txHash = txHash;
|
|
165
|
+
}
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
119
167
|
// Internal helpers
|
|
120
168
|
// -------------------------------------------------------------------------
|
|
121
169
|
/**
|
|
@@ -591,6 +639,11 @@ var RelayService = class {
|
|
|
591
639
|
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
592
640
|
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
593
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.
|
|
594
647
|
*/
|
|
595
648
|
async submitMintAndSwap(params) {
|
|
596
649
|
if (this.simulateBeforeSubmit && this.provider) {
|
|
@@ -669,84 +722,35 @@ var DEFAULT_GAS_UNITS = 500000n;
|
|
|
669
722
|
var DEFAULT_PREMIUM_BPS = 12e3;
|
|
670
723
|
var FeeManager = class {
|
|
671
724
|
provider;
|
|
672
|
-
|
|
673
|
-
mintAndSwapGasUnits;
|
|
725
|
+
gasUnits;
|
|
674
726
|
gasPremiumBps;
|
|
675
|
-
|
|
676
|
-
rebalanceThresholdWei;
|
|
677
|
-
rebalanceUsdtAmount;
|
|
678
|
-
swapUsdtToNative;
|
|
727
|
+
quoteNativeToFee;
|
|
679
728
|
constructor(config) {
|
|
680
729
|
if (!config.provider) throw new Error("FeeManager: provider required");
|
|
681
|
-
if (!config.
|
|
682
|
-
throw new Error("FeeManager:
|
|
683
|
-
if (!config.quoteNativeToUsdt)
|
|
684
|
-
throw new Error("FeeManager: quoteNativeToUsdt required");
|
|
730
|
+
if (!config.quoteNativeToFee)
|
|
731
|
+
throw new Error("FeeManager: quoteNativeToFee required");
|
|
685
732
|
this.provider = config.provider;
|
|
686
|
-
this.
|
|
687
|
-
this.mintAndSwapGasUnits = config.mintAndSwapGasUnits ?? DEFAULT_GAS_UNITS;
|
|
733
|
+
this.gasUnits = config.gasUnits ?? DEFAULT_GAS_UNITS;
|
|
688
734
|
this.gasPremiumBps = config.gasPremiumBps ?? DEFAULT_PREMIUM_BPS;
|
|
689
|
-
this.
|
|
690
|
-
if (config.rebalanceThresholdWei !== void 0) {
|
|
691
|
-
this.rebalanceThresholdWei = config.rebalanceThresholdWei;
|
|
692
|
-
}
|
|
693
|
-
if (config.rebalanceUsdtAmount !== void 0) {
|
|
694
|
-
this.rebalanceUsdtAmount = config.rebalanceUsdtAmount;
|
|
695
|
-
}
|
|
696
|
-
if (config.swapUsdtToNative) {
|
|
697
|
-
this.swapUsdtToNative = config.swapUsdtToNative;
|
|
698
|
-
}
|
|
699
|
-
const rebalanceFields = [
|
|
700
|
-
config.rebalanceThresholdWei,
|
|
701
|
-
config.rebalanceUsdtAmount,
|
|
702
|
-
config.swapUsdtToNative
|
|
703
|
-
];
|
|
704
|
-
const someSet = rebalanceFields.some((v) => v !== void 0);
|
|
705
|
-
const allSet = rebalanceFields.every((v) => v !== void 0);
|
|
706
|
-
if (someSet && !allSet) {
|
|
707
|
-
throw new Error(
|
|
708
|
-
"FeeManager: rebalanceThresholdWei, rebalanceUsdtAmount, and swapUsdtToNative must all be set together"
|
|
709
|
-
);
|
|
710
|
-
}
|
|
735
|
+
this.quoteNativeToFee = config.quoteNativeToFee;
|
|
711
736
|
}
|
|
712
737
|
/**
|
|
713
|
-
* Estimate the
|
|
714
|
-
*
|
|
738
|
+
* Estimate the fee (in the caller's fee currency) to charge for the
|
|
739
|
+
* next sponsored UserOp:
|
|
715
740
|
*
|
|
716
|
-
* nativeCost
|
|
717
|
-
*
|
|
718
|
-
*
|
|
741
|
+
* nativeCost = gasUnits × gasPrice
|
|
742
|
+
* withPremium = nativeCost × premiumBps / 10_000
|
|
743
|
+
* fee = quoteNativeToFee(withPremium)
|
|
744
|
+
*
|
|
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`.
|
|
719
748
|
*/
|
|
720
749
|
async estimateGasFee() {
|
|
721
750
|
const gasPrice = await this.provider.getGasPrice();
|
|
722
|
-
const nativeCost = gasPrice * this.
|
|
751
|
+
const nativeCost = gasPrice * this.gasUnits;
|
|
723
752
|
const withPremium = nativeCost * BigInt(this.gasPremiumBps) / 10000n;
|
|
724
|
-
return this.
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Check the operator's native balance and, if it has dropped below the
|
|
728
|
-
* configured threshold, trigger a USDT→native rebalance via the injected
|
|
729
|
-
* `swapUsdtToNative` function.
|
|
730
|
-
*
|
|
731
|
-
* Returns `true` if a rebalance was performed, `false` otherwise.
|
|
732
|
-
* Silently no-ops when rebalance is not configured.
|
|
733
|
-
*/
|
|
734
|
-
async rebalanceIfNeeded() {
|
|
735
|
-
if (this.rebalanceThresholdWei === void 0 || this.rebalanceUsdtAmount === void 0 || !this.swapUsdtToNative) {
|
|
736
|
-
return false;
|
|
737
|
-
}
|
|
738
|
-
const operatorAddress = this.operatorWallet.account?.address;
|
|
739
|
-
if (!operatorAddress) {
|
|
740
|
-
throw new Error(
|
|
741
|
-
"FeeManager: operator wallet has no account attached \u2014 cannot read balance"
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
const balance = await this.provider.getBalance({ address: operatorAddress });
|
|
745
|
-
if (balance >= this.rebalanceThresholdWei) {
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
await this.swapUsdtToNative(this.rebalanceUsdtAmount);
|
|
749
|
-
return true;
|
|
753
|
+
return this.quoteNativeToFee(withPremium);
|
|
750
754
|
}
|
|
751
755
|
};
|
|
752
756
|
|
|
@@ -795,6 +799,12 @@ var MintingGateway = class {
|
|
|
795
799
|
this.now = config.now ?? (() => Date.now());
|
|
796
800
|
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
797
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
|
+
*/
|
|
798
808
|
async processMintAndCashOut(request) {
|
|
799
809
|
const { receiverConsent, receiverSignature } = request;
|
|
800
810
|
if (!receiverConsent || !receiverSignature) {
|
|
@@ -1184,8 +1194,159 @@ function pickMatchingLock(locks, amount) {
|
|
|
1184
1194
|
return best;
|
|
1185
1195
|
}
|
|
1186
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
|
+
|
|
1187
1348
|
// src/api/handlers.ts
|
|
1188
|
-
import { getAddress as
|
|
1349
|
+
import { getAddress as getAddress6 } from "viem";
|
|
1189
1350
|
import {
|
|
1190
1351
|
getMintRequestNonce,
|
|
1191
1352
|
getPointTokenBalance,
|
|
@@ -1210,6 +1371,7 @@ var IssuerApiHandlers = class {
|
|
|
1210
1371
|
defaultToken;
|
|
1211
1372
|
chainId;
|
|
1212
1373
|
contracts;
|
|
1374
|
+
pafiWebUrl;
|
|
1213
1375
|
feeManager;
|
|
1214
1376
|
poolsProvider;
|
|
1215
1377
|
constructor(config) {
|
|
@@ -1223,11 +1385,12 @@ var IssuerApiHandlers = class {
|
|
|
1223
1385
|
"IssuerApiHandlers: pointTokenAddress or pointTokenAddresses required"
|
|
1224
1386
|
);
|
|
1225
1387
|
}
|
|
1226
|
-
const normalized = raw.map((a) =>
|
|
1388
|
+
const normalized = raw.map((a) => getAddress6(a));
|
|
1227
1389
|
this.supportedTokens = new Set(normalized);
|
|
1228
1390
|
this.defaultToken = normalized[0];
|
|
1229
1391
|
this.chainId = config.chainId;
|
|
1230
1392
|
this.contracts = config.contracts;
|
|
1393
|
+
if (config.pafiWebUrl) this.pafiWebUrl = config.pafiWebUrl;
|
|
1231
1394
|
if (config.feeManager) this.feeManager = config.feeManager;
|
|
1232
1395
|
if (config.poolsProvider) this.poolsProvider = config.poolsProvider;
|
|
1233
1396
|
}
|
|
@@ -1263,7 +1426,16 @@ var IssuerApiHandlers = class {
|
|
|
1263
1426
|
`handleConfig: unsupported chainId ${chainId}, issuer is configured for ${this.chainId}`
|
|
1264
1427
|
);
|
|
1265
1428
|
}
|
|
1266
|
-
|
|
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;
|
|
1267
1439
|
}
|
|
1268
1440
|
/** `GET /gas-fee` — quoted in USDT (6-decimal base units). */
|
|
1269
1441
|
async handleGasFee() {
|
|
@@ -1314,14 +1486,14 @@ var IssuerApiHandlers = class {
|
|
|
1314
1486
|
`handleUser: unsupported chainId ${request.chainId}`
|
|
1315
1487
|
);
|
|
1316
1488
|
}
|
|
1317
|
-
const normalizedAuthed =
|
|
1318
|
-
const normalizedRequest =
|
|
1489
|
+
const normalizedAuthed = getAddress6(userAddress);
|
|
1490
|
+
const normalizedRequest = getAddress6(request.userAddress);
|
|
1319
1491
|
if (normalizedAuthed !== normalizedRequest) {
|
|
1320
1492
|
throw new Error(
|
|
1321
1493
|
"handleUser: request userAddress must match authenticated user"
|
|
1322
1494
|
);
|
|
1323
1495
|
}
|
|
1324
|
-
const pointToken =
|
|
1496
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1325
1497
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1326
1498
|
throw new Error(
|
|
1327
1499
|
`handleUser: unsupported pointToken ${pointToken}`
|
|
@@ -1364,7 +1536,7 @@ var IssuerApiHandlers = class {
|
|
|
1364
1536
|
`handleBuildConsentTypedData: unsupported chainId ${request.chainId}`
|
|
1365
1537
|
);
|
|
1366
1538
|
}
|
|
1367
|
-
const pointToken =
|
|
1539
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1368
1540
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1369
1541
|
throw new Error(
|
|
1370
1542
|
`handleBuildConsentTypedData: unsupported pointToken ${pointToken}`
|
|
@@ -1389,8 +1561,14 @@ var IssuerApiHandlers = class {
|
|
|
1389
1561
|
/**
|
|
1390
1562
|
* `POST /claim-and-swap`
|
|
1391
1563
|
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
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.
|
|
1394
1572
|
*/
|
|
1395
1573
|
async handleClaimAndSwap(userAddress, request) {
|
|
1396
1574
|
if (request.chainId !== this.chainId) {
|
|
@@ -1398,14 +1576,14 @@ var IssuerApiHandlers = class {
|
|
|
1398
1576
|
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1399
1577
|
);
|
|
1400
1578
|
}
|
|
1401
|
-
const pointToken =
|
|
1579
|
+
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1402
1580
|
if (!this.supportedTokens.has(pointToken)) {
|
|
1403
1581
|
throw new Error(
|
|
1404
1582
|
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1405
1583
|
);
|
|
1406
1584
|
}
|
|
1407
1585
|
const result = await this.gateway.processMintAndCashOut({
|
|
1408
|
-
userAddress:
|
|
1586
|
+
userAddress: getAddress6(userAddress),
|
|
1409
1587
|
pointTokenAddress: pointToken,
|
|
1410
1588
|
chainId: request.chainId,
|
|
1411
1589
|
domain: request.domain,
|
|
@@ -1636,8 +1814,274 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1636
1814
|
return BigInt(whole + padded);
|
|
1637
1815
|
}
|
|
1638
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
|
+
|
|
1639
2083
|
// src/config.ts
|
|
1640
|
-
import { getAddress as
|
|
2084
|
+
import { getAddress as getAddress7 } from "viem";
|
|
1641
2085
|
function createIssuerService(config) {
|
|
1642
2086
|
if (!config.provider) {
|
|
1643
2087
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -1663,7 +2107,7 @@ function createIssuerService(config) {
|
|
|
1663
2107
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
1664
2108
|
);
|
|
1665
2109
|
}
|
|
1666
|
-
const tokenAddresses = rawAddresses.map((a) =>
|
|
2110
|
+
const tokenAddresses = rawAddresses.map((a) => getAddress7(a));
|
|
1667
2111
|
const ledger = config.ledger ?? new MemoryPointLedger();
|
|
1668
2112
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
1669
2113
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
@@ -1693,8 +2137,7 @@ function createIssuerService(config) {
|
|
|
1693
2137
|
if (config.fee) {
|
|
1694
2138
|
feeManager = new FeeManager({
|
|
1695
2139
|
...config.fee,
|
|
1696
|
-
provider: config.provider
|
|
1697
|
-
operatorWallet: config.operatorWallet
|
|
2140
|
+
provider: config.provider
|
|
1698
2141
|
});
|
|
1699
2142
|
}
|
|
1700
2143
|
const gatewayConfig = {
|
|
@@ -1769,6 +2212,8 @@ var PAFI_ISSUER_SDK_VERSION = "0.1.0";
|
|
|
1769
2212
|
export {
|
|
1770
2213
|
AuthError,
|
|
1771
2214
|
AuthService,
|
|
2215
|
+
BalanceAggregator,
|
|
2216
|
+
BurnIndexer,
|
|
1772
2217
|
DefaultPolicyEngine,
|
|
1773
2218
|
FeeManager,
|
|
1774
2219
|
InMemoryCursorStore,
|
|
@@ -1779,6 +2224,8 @@ export {
|
|
|
1779
2224
|
MintingGatewayError,
|
|
1780
2225
|
NonceManager,
|
|
1781
2226
|
PAFI_ISSUER_SDK_VERSION,
|
|
2227
|
+
PafiBackendClient,
|
|
2228
|
+
PafiBackendError,
|
|
1782
2229
|
PointIndexer,
|
|
1783
2230
|
PrivateKeySigner,
|
|
1784
2231
|
RelayError,
|