@pafi-dev/issuer 0.3.0-alpha.0 → 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +354 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -2
- package/dist/index.d.ts +208 -2
- package/dist/index.js +338 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -582,11 +582,15 @@ var RelayError = class extends Error {
|
|
|
582
582
|
};
|
|
583
583
|
|
|
584
584
|
// src/relay/relayService.ts
|
|
585
|
+
import { encodeFunctionData } from "viem";
|
|
585
586
|
import {
|
|
586
587
|
relayAbi,
|
|
587
588
|
encodeMintAndSwap,
|
|
588
589
|
simulateMintAndSwap as coreSimulateMintAndSwap,
|
|
589
|
-
SimulationError
|
|
590
|
+
SimulationError,
|
|
591
|
+
RELAYER_V2_ABI,
|
|
592
|
+
POINT_TOKEN_V2_ABI,
|
|
593
|
+
buildPartialUserOperation
|
|
590
594
|
} from "@pafi-dev/core";
|
|
591
595
|
var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
592
596
|
var RelayService = class {
|
|
@@ -712,6 +716,154 @@ var RelayService = class {
|
|
|
712
716
|
);
|
|
713
717
|
}
|
|
714
718
|
}
|
|
719
|
+
// ==========================================================================
|
|
720
|
+
// v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
|
|
721
|
+
// ==========================================================================
|
|
722
|
+
//
|
|
723
|
+
// These two methods build unsigned `PartialUserOperation` payloads for
|
|
724
|
+
// the Frontend to sign (via Privy) and submit to the Bundler. The
|
|
725
|
+
// Issuer Backend no longer broadcasts — that's the Frontend's job.
|
|
726
|
+
//
|
|
727
|
+
// Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
|
|
728
|
+
// When SC delivers real ABIs, the imports swap but these method bodies
|
|
729
|
+
// stay the same (calldata encoder is ABI-driven).
|
|
730
|
+
// ==========================================================================
|
|
731
|
+
/**
|
|
732
|
+
* Build an unsigned UserOp for Scenario 1 (Mint).
|
|
733
|
+
*
|
|
734
|
+
* Flow:
|
|
735
|
+
* 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
|
|
736
|
+
* 2. Optionally append a PT fee transfer from user → feeRecipient
|
|
737
|
+
* (fee recovery happens on-chain via BatchExecutor, not via an
|
|
738
|
+
* operator wallet)
|
|
739
|
+
* 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
|
|
740
|
+
* 4. Return a `PartialUserOperation` ready for:
|
|
741
|
+
* - gas estimation (Bundler)
|
|
742
|
+
* - paymaster sponsorship (PAFI Backend)
|
|
743
|
+
* - user signature (Privy)
|
|
744
|
+
*/
|
|
745
|
+
prepareMint(params) {
|
|
746
|
+
if (!params.relayerAddress) {
|
|
747
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
|
|
748
|
+
}
|
|
749
|
+
if (!params.batchExecutorAddress) {
|
|
750
|
+
throw new RelayError(
|
|
751
|
+
"ENCODE_FAILED",
|
|
752
|
+
"prepareMint: batchExecutorAddress required"
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (!params.userAddress) {
|
|
756
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
|
|
757
|
+
}
|
|
758
|
+
let mintCallData;
|
|
759
|
+
try {
|
|
760
|
+
mintCallData = encodeFunctionData({
|
|
761
|
+
abi: RELAYER_V2_ABI,
|
|
762
|
+
functionName: "mint",
|
|
763
|
+
args: [params.mintRequest, params.userSignature, params.issuerSignature]
|
|
764
|
+
});
|
|
765
|
+
} catch (err) {
|
|
766
|
+
throw new RelayError(
|
|
767
|
+
"ENCODE_FAILED",
|
|
768
|
+
`prepareMint: failed to encode Relayer.mint: ${errorMessage(err)}`,
|
|
769
|
+
err
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const operations = [
|
|
773
|
+
{
|
|
774
|
+
target: params.relayerAddress,
|
|
775
|
+
value: 0n,
|
|
776
|
+
data: mintCallData
|
|
777
|
+
}
|
|
778
|
+
];
|
|
779
|
+
if (params.mintRequest.feeAmount > 0n) {
|
|
780
|
+
operations.push({
|
|
781
|
+
target: params.pointTokenAddress,
|
|
782
|
+
value: 0n,
|
|
783
|
+
data: encodeFunctionData({
|
|
784
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
785
|
+
functionName: "balanceOf",
|
|
786
|
+
// placeholder — real impl uses transfer
|
|
787
|
+
args: [params.mintRequest.feeRecipient]
|
|
788
|
+
})
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return buildPartialUserOperation({
|
|
792
|
+
sender: params.userAddress,
|
|
793
|
+
nonce: params.aaNonce,
|
|
794
|
+
operations,
|
|
795
|
+
gasLimits: {
|
|
796
|
+
callGasLimit: params.callGasLimit ?? 500000n,
|
|
797
|
+
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
798
|
+
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
|
|
804
|
+
*
|
|
805
|
+
* Two modes:
|
|
806
|
+
* - `mode: 'burn'` — direct `PointToken.burn(amount)`; `msg.sender`
|
|
807
|
+
* via EIP-7702 delegation is the user, so no signature needed
|
|
808
|
+
* on-chain (the BurnConsent was already verified off-chain by
|
|
809
|
+
* the issuer backend before we got here)
|
|
810
|
+
* - `mode: 'burnWithSig'` — `PointToken.burnWithSig(consent, sig)`;
|
|
811
|
+
* used when the issuer hasn't verified the consent and the
|
|
812
|
+
* contract has to do it on-chain
|
|
813
|
+
*/
|
|
814
|
+
prepareBurn(params) {
|
|
815
|
+
if (!params.pointTokenAddress) {
|
|
816
|
+
throw new RelayError("ENCODE_FAILED", "prepareBurn: pointTokenAddress required");
|
|
817
|
+
}
|
|
818
|
+
if (!params.batchExecutorAddress) {
|
|
819
|
+
throw new RelayError(
|
|
820
|
+
"ENCODE_FAILED",
|
|
821
|
+
"prepareBurn: batchExecutorAddress required"
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
let burnCallData;
|
|
825
|
+
try {
|
|
826
|
+
if (params.mode === "burnWithSig") {
|
|
827
|
+
if (!params.burnConsent || !params.consentSignature) {
|
|
828
|
+
throw new Error("burnWithSig requires burnConsent + consentSignature");
|
|
829
|
+
}
|
|
830
|
+
burnCallData = encodeFunctionData({
|
|
831
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
832
|
+
functionName: "burnWithSig",
|
|
833
|
+
args: [params.burnConsent, params.consentSignature]
|
|
834
|
+
});
|
|
835
|
+
} else {
|
|
836
|
+
burnCallData = encodeFunctionData({
|
|
837
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
838
|
+
functionName: "burn",
|
|
839
|
+
args: [params.amount]
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
} catch (err) {
|
|
843
|
+
throw new RelayError(
|
|
844
|
+
"ENCODE_FAILED",
|
|
845
|
+
`prepareBurn: failed to encode burn call: ${errorMessage(err)}`,
|
|
846
|
+
err
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const operations = [
|
|
850
|
+
{
|
|
851
|
+
target: params.pointTokenAddress,
|
|
852
|
+
value: 0n,
|
|
853
|
+
data: burnCallData
|
|
854
|
+
}
|
|
855
|
+
];
|
|
856
|
+
return buildPartialUserOperation({
|
|
857
|
+
sender: params.userAddress,
|
|
858
|
+
nonce: params.aaNonce,
|
|
859
|
+
operations,
|
|
860
|
+
gasLimits: {
|
|
861
|
+
callGasLimit: params.callGasLimit ?? 300000n,
|
|
862
|
+
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
863
|
+
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
715
867
|
};
|
|
716
868
|
function errorMessage(err) {
|
|
717
869
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -1603,6 +1755,183 @@ var IssuerApiHandlers = class {
|
|
|
1603
1755
|
}
|
|
1604
1756
|
};
|
|
1605
1757
|
|
|
1758
|
+
// src/api/handlers/ptRedeemHandler.ts
|
|
1759
|
+
import { getAddress as getAddress7 } from "viem";
|
|
1760
|
+
import { verifyBurnConsent } from "@pafi-dev/core";
|
|
1761
|
+
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1762
|
+
var PTRedeemError = class extends Error {
|
|
1763
|
+
constructor(code, message) {
|
|
1764
|
+
super(message);
|
|
1765
|
+
this.code = code;
|
|
1766
|
+
this.name = "PTRedeemError";
|
|
1767
|
+
}
|
|
1768
|
+
code;
|
|
1769
|
+
};
|
|
1770
|
+
var PTRedeemHandler = class {
|
|
1771
|
+
ledger;
|
|
1772
|
+
relayService;
|
|
1773
|
+
pointTokenAddress;
|
|
1774
|
+
batchExecutorAddress;
|
|
1775
|
+
chainId;
|
|
1776
|
+
domain;
|
|
1777
|
+
redeemLockDurationMs;
|
|
1778
|
+
now;
|
|
1779
|
+
constructor(config) {
|
|
1780
|
+
if (!config.ledger.reservePendingCredit) {
|
|
1781
|
+
throw new PTRedeemError(
|
|
1782
|
+
"LEDGER_NOT_SUPPORTED",
|
|
1783
|
+
"PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
this.ledger = config.ledger;
|
|
1787
|
+
this.relayService = config.relayService;
|
|
1788
|
+
this.pointTokenAddress = getAddress7(config.pointTokenAddress);
|
|
1789
|
+
this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
|
|
1790
|
+
this.chainId = config.chainId;
|
|
1791
|
+
this.domain = config.domain;
|
|
1792
|
+
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1793
|
+
this.now = config.now ?? (() => Date.now());
|
|
1794
|
+
}
|
|
1795
|
+
async handle(request) {
|
|
1796
|
+
if (request.amount <= 0n) {
|
|
1797
|
+
throw new PTRedeemError("INVALID_CONSENT", "redeem amount must be positive");
|
|
1798
|
+
}
|
|
1799
|
+
if (request.consent.amount !== request.amount) {
|
|
1800
|
+
throw new PTRedeemError(
|
|
1801
|
+
"AMOUNT_MISMATCH",
|
|
1802
|
+
`consent.amount (${request.consent.amount}) must match request.amount (${request.amount})`
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
const nowSeconds = BigInt(Math.floor(this.now() / 1e3));
|
|
1806
|
+
if (request.consent.deadline <= nowSeconds) {
|
|
1807
|
+
throw new PTRedeemError(
|
|
1808
|
+
"EXPIRED_CONSENT",
|
|
1809
|
+
`consent deadline (${request.consent.deadline}) already passed`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
const verification = await verifyBurnConsent(
|
|
1813
|
+
{
|
|
1814
|
+
name: this.domain.name,
|
|
1815
|
+
chainId: this.chainId,
|
|
1816
|
+
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1817
|
+
},
|
|
1818
|
+
request.consent,
|
|
1819
|
+
request.consentSignature,
|
|
1820
|
+
request.userAddress
|
|
1821
|
+
);
|
|
1822
|
+
if (!verification.isValid) {
|
|
1823
|
+
throw new PTRedeemError(
|
|
1824
|
+
"SIGNATURE_MISMATCH",
|
|
1825
|
+
`signer mismatch \u2014 expected ${request.userAddress}, got ${verification.recoveredAddress}`
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const lockId = await this.ledger.reservePendingCredit(
|
|
1829
|
+
request.userAddress,
|
|
1830
|
+
request.amount,
|
|
1831
|
+
this.redeemLockDurationMs,
|
|
1832
|
+
this.pointTokenAddress
|
|
1833
|
+
);
|
|
1834
|
+
const userOp = this.relayService.prepareBurn({
|
|
1835
|
+
mode: "burnWithSig",
|
|
1836
|
+
userAddress: request.userAddress,
|
|
1837
|
+
aaNonce: request.aaNonce,
|
|
1838
|
+
pointTokenAddress: this.pointTokenAddress,
|
|
1839
|
+
batchExecutorAddress: this.batchExecutorAddress,
|
|
1840
|
+
burnConsent: request.consent,
|
|
1841
|
+
consentSignature: parseSigStruct(request.consentSignature)
|
|
1842
|
+
});
|
|
1843
|
+
return {
|
|
1844
|
+
lockId,
|
|
1845
|
+
userOp,
|
|
1846
|
+
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
function parseSigStruct(serialized) {
|
|
1851
|
+
const raw = serialized.slice(2);
|
|
1852
|
+
if (raw.length !== 130) {
|
|
1853
|
+
throw new PTRedeemError(
|
|
1854
|
+
"INVALID_CONSENT",
|
|
1855
|
+
`signature must be 65 bytes, got ${raw.length / 2}`
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
const r = `0x${raw.slice(0, 64)}`;
|
|
1859
|
+
const s = `0x${raw.slice(64, 128)}`;
|
|
1860
|
+
const v = parseInt(raw.slice(128, 130), 16);
|
|
1861
|
+
return { v, r, s };
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1865
|
+
import { getAddress as getAddress8 } from "viem";
|
|
1866
|
+
import { getPointTokenBalance as getPointTokenBalance2 } from "@pafi-dev/core";
|
|
1867
|
+
var TopUpRedemptionError = class extends Error {
|
|
1868
|
+
constructor(code, message) {
|
|
1869
|
+
super(message);
|
|
1870
|
+
this.code = code;
|
|
1871
|
+
this.name = "TopUpRedemptionError";
|
|
1872
|
+
}
|
|
1873
|
+
code;
|
|
1874
|
+
};
|
|
1875
|
+
var TopUpRedemptionHandler = class {
|
|
1876
|
+
ledger;
|
|
1877
|
+
ptRedeemHandler;
|
|
1878
|
+
provider;
|
|
1879
|
+
pointTokenAddress;
|
|
1880
|
+
constructor(config) {
|
|
1881
|
+
this.ledger = config.ledger;
|
|
1882
|
+
this.ptRedeemHandler = config.ptRedeemHandler;
|
|
1883
|
+
this.provider = config.provider;
|
|
1884
|
+
this.pointTokenAddress = getAddress8(config.pointTokenAddress);
|
|
1885
|
+
}
|
|
1886
|
+
async handle(request) {
|
|
1887
|
+
const offChainBalance = await this.ledger.getBalance(
|
|
1888
|
+
request.userAddress,
|
|
1889
|
+
this.pointTokenAddress
|
|
1890
|
+
);
|
|
1891
|
+
if (offChainBalance >= request.requiredAmount) {
|
|
1892
|
+
return { action: "NO_TOP_UP_NEEDED", offChainBalance };
|
|
1893
|
+
}
|
|
1894
|
+
const shortfall = request.requiredAmount - offChainBalance;
|
|
1895
|
+
const onChainBalance = await getPointTokenBalance2(
|
|
1896
|
+
this.provider,
|
|
1897
|
+
this.pointTokenAddress,
|
|
1898
|
+
request.userAddress
|
|
1899
|
+
);
|
|
1900
|
+
if (onChainBalance < shortfall) {
|
|
1901
|
+
return {
|
|
1902
|
+
action: "INSUFFICIENT_ONCHAIN",
|
|
1903
|
+
offChainBalance,
|
|
1904
|
+
onChainBalance,
|
|
1905
|
+
shortfall
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
if (request.redeemRequest.consent.amount < shortfall) {
|
|
1909
|
+
throw new TopUpRedemptionError(
|
|
1910
|
+
"CONSENT_AMOUNT_TOO_LOW",
|
|
1911
|
+
`consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
if (request.redeemRequest.consent.amount !== shortfall) {
|
|
1915
|
+
throw new TopUpRedemptionError(
|
|
1916
|
+
"CONSENT_AMOUNT_TOO_LOW",
|
|
1917
|
+
`consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
const redeem = await this.ptRedeemHandler.handle({
|
|
1921
|
+
userAddress: request.userAddress,
|
|
1922
|
+
amount: shortfall,
|
|
1923
|
+
consent: request.redeemRequest.consent,
|
|
1924
|
+
consentSignature: request.redeemRequest.consentSignature,
|
|
1925
|
+
aaNonce: request.redeemRequest.aaNonce
|
|
1926
|
+
});
|
|
1927
|
+
return {
|
|
1928
|
+
action: "TOP_UP_STARTED",
|
|
1929
|
+
shortfall,
|
|
1930
|
+
redeem
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1606
1935
|
// src/pools/subgraphPoolsProvider.ts
|
|
1607
1936
|
var DEFAULT_CACHE_TTL_MS = 3e4;
|
|
1608
1937
|
var POOL_QUERY = `
|
|
@@ -1815,7 +2144,7 @@ function toUsdtPerNative(priceFloat, usdtDecimals) {
|
|
|
1815
2144
|
}
|
|
1816
2145
|
|
|
1817
2146
|
// src/balance/balanceAggregator.ts
|
|
1818
|
-
import { getPointTokenBalance as
|
|
2147
|
+
import { getPointTokenBalance as getPointTokenBalance3 } from "@pafi-dev/core";
|
|
1819
2148
|
var BalanceAggregator = class {
|
|
1820
2149
|
provider;
|
|
1821
2150
|
ledger;
|
|
@@ -1836,7 +2165,7 @@ var BalanceAggregator = class {
|
|
|
1836
2165
|
async getCombinedBalance(user, pointToken) {
|
|
1837
2166
|
const [offChain, onChain] = await Promise.all([
|
|
1838
2167
|
this.ledger.getBalance(user, pointToken),
|
|
1839
|
-
|
|
2168
|
+
getPointTokenBalance3(this.provider, pointToken, user)
|
|
1840
2169
|
]);
|
|
1841
2170
|
return {
|
|
1842
2171
|
offChain,
|
|
@@ -2081,7 +2410,7 @@ var PafiBackendClient = class {
|
|
|
2081
2410
|
};
|
|
2082
2411
|
|
|
2083
2412
|
// src/config.ts
|
|
2084
|
-
import { getAddress as
|
|
2413
|
+
import { getAddress as getAddress9 } from "viem";
|
|
2085
2414
|
function createIssuerService(config) {
|
|
2086
2415
|
if (!config.provider) {
|
|
2087
2416
|
throw new Error("createIssuerService: provider is required");
|
|
@@ -2107,7 +2436,7 @@ function createIssuerService(config) {
|
|
|
2107
2436
|
"createIssuerService: at least one of pointTokenAddress / pointTokenAddresses is required"
|
|
2108
2437
|
);
|
|
2109
2438
|
}
|
|
2110
|
-
const tokenAddresses = rawAddresses.map((a) =>
|
|
2439
|
+
const tokenAddresses = rawAddresses.map((a) => getAddress9(a));
|
|
2111
2440
|
const ledger = config.ledger ?? new MemoryPointLedger();
|
|
2112
2441
|
const sessionStore = config.sessionStore ?? new MemorySessionStore();
|
|
2113
2442
|
const policy = config.policy ?? new DefaultPolicyEngine({ ledger });
|
|
@@ -2224,12 +2553,16 @@ export {
|
|
|
2224
2553
|
MintingGatewayError,
|
|
2225
2554
|
NonceManager,
|
|
2226
2555
|
PAFI_ISSUER_SDK_VERSION,
|
|
2556
|
+
PTRedeemError,
|
|
2557
|
+
PTRedeemHandler,
|
|
2227
2558
|
PafiBackendClient,
|
|
2228
2559
|
PafiBackendError,
|
|
2229
2560
|
PointIndexer,
|
|
2230
2561
|
PrivateKeySigner,
|
|
2231
2562
|
RelayError,
|
|
2232
2563
|
RelayService,
|
|
2564
|
+
TopUpRedemptionError,
|
|
2565
|
+
TopUpRedemptionHandler,
|
|
2233
2566
|
authenticateRequest,
|
|
2234
2567
|
createIssuerService,
|
|
2235
2568
|
createSubgraphNativeUsdtQuoter,
|