@gvnrdao/dh-sdk 0.0.338 → 0.0.340
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/browser/dist/browser.js +1 -1
- package/dist/graphs/diamond-hands.d.ts +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1138 -182
- package/dist/index.mjs +1073 -117
- package/dist/interfaces/chunks/loan-operations.i.d.ts +34 -0
- package/dist/modules/diamond-hands-sdk.d.ts +22 -5
- package/dist/modules/loan/loan-query.module.d.ts +12 -1
- package/dist/sign-guard.js +141 -2
- package/dist/sign-guard.mjs +135 -2
- package/dist/utils/address-conversion.utils.d.ts +15 -6
- package/dist/utils/agent-delegation.utils.d.ts +39 -0
- package/dist/utils/assert-provider-chain.d.ts +16 -0
- package/dist/utils/borrower-ucd-debt-summary.d.ts +44 -0
- package/dist/utils/concurrency-limiter.utils.d.ts +59 -0
- package/dist/utils/lit-action-chain-name.d.ts +14 -0
- package/dist/utils/loan-helpers.utils.d.ts +115 -0
- package/dist/utils/sign-guard/errors.d.ts +5 -1
- package/dist/utils/sign-guard/index.d.ts +1 -0
- package/dist/utils/sign-guard/psm-exchange.d.ts +122 -0
- package/package.json +3 -4
package/dist/index.mjs
CHANGED
|
@@ -2357,7 +2357,7 @@ ${errorReport}`);
|
|
|
2357
2357
|
}
|
|
2358
2358
|
init_debug_logger();
|
|
2359
2359
|
init_session_signature_cache();
|
|
2360
|
-
var
|
|
2360
|
+
var import_ethers23 = __require("ethers");
|
|
2361
2361
|
var EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS = 11e3;
|
|
2362
2362
|
var GRACE_PERIOD_DAYS = 30;
|
|
2363
2363
|
var SATOSHIS_PER_BITCOIN = 100000000n;
|
|
@@ -5285,12 +5285,12 @@ ${auth.clientId}`;
|
|
|
5285
5285
|
}
|
|
5286
5286
|
async function executeVaultSnapshot(params) {
|
|
5287
5287
|
global.ethers = {
|
|
5288
|
-
...
|
|
5288
|
+
...import_ethers23.ethers,
|
|
5289
5289
|
providers: {
|
|
5290
|
-
StaticJsonRpcProvider:
|
|
5290
|
+
StaticJsonRpcProvider: import_ethers23.ethers.JsonRpcProvider
|
|
5291
5291
|
},
|
|
5292
|
-
Contract:
|
|
5293
|
-
utils:
|
|
5292
|
+
Contract: import_ethers23.ethers.Contract,
|
|
5293
|
+
utils: import_ethers23.ethers
|
|
5294
5294
|
// v6 moved utils to top level
|
|
5295
5295
|
};
|
|
5296
5296
|
global.Lit = {
|
|
@@ -5546,8 +5546,8 @@ function getSepoliaConfig() {
|
|
|
5546
5546
|
positionManagerCore: SEPOLIA_CONTRACTS.PositionManagerCoreModule || "",
|
|
5547
5547
|
positionManagerViews: SEPOLIA_CONTRACTS.PositionManagerViews || "",
|
|
5548
5548
|
simplePsmV2: SEPOLIA_CONTRACTS.SimplePSMV2 || "",
|
|
5549
|
-
mockUsdcToken: SEPOLIA_CONTRACTS
|
|
5550
|
-
mockUsdtToken: SEPOLIA_CONTRACTS
|
|
5549
|
+
mockUsdcToken: SEPOLIA_CONTRACTS.mockUsdcToken || "",
|
|
5550
|
+
mockUsdtToken: SEPOLIA_CONTRACTS.mockUsdtToken || "",
|
|
5551
5551
|
loanOperationsManager: SEPOLIA_CONTRACTS.LoanOperationsManagerModule || "",
|
|
5552
5552
|
termManager: SEPOLIA_CONTRACTS.TermManagerModule || "",
|
|
5553
5553
|
circuitBreaker: SEPOLIA_CONTRACTS.CircuitBreakerModule || "",
|
|
@@ -5623,14 +5623,17 @@ function getMainnetConfig() {
|
|
|
5623
5623
|
agentModuleFactory: MAINNET_CONTRACTS.AgentModuleFactory || ""
|
|
5624
5624
|
},
|
|
5625
5625
|
subgraphs: {
|
|
5626
|
-
//
|
|
5627
|
-
//
|
|
5628
|
-
//
|
|
5629
|
-
//
|
|
5630
|
-
//
|
|
5631
|
-
//
|
|
5632
|
-
//
|
|
5633
|
-
|
|
5626
|
+
// The MAINNET subgraph on The Graph's decentralized network — the same id
|
|
5627
|
+
// lit-ops-server's ETHEREUM_SUBGRAPH_URL points at (infra/terraform). The path segment is
|
|
5628
|
+
// the subgraph's PUBLIC ID, not a credential. This used to be a SEPOLIA id under a
|
|
5629
|
+
// chainId-1 config: unusable as shipped, but a caller who added their own key would have
|
|
5630
|
+
// read Sepolia data as mainnet.
|
|
5631
|
+
//
|
|
5632
|
+
// The gateway needs an `Authorization: Bearer <query key>` the SDK does not (and must
|
|
5633
|
+
// not) hold, so this URL only works for a standalone caller supplying their own key.
|
|
5634
|
+
// Service mode never uses it — queries go through the lit-ops-server proxy, which adds
|
|
5635
|
+
// the key server-side. Same rule as rpcUrls above: never ship a keyed URL in a client config.
|
|
5636
|
+
diamondHandsUrl: "https://gateway.thegraph.com/api/subgraphs/id/8Gt9zaSCgkxxSiLMGaKVj7qU3ds1heGVGxgWcXXpwbPd"
|
|
5634
5637
|
},
|
|
5635
5638
|
litNetwork: "chipotle",
|
|
5636
5639
|
debug: false
|
|
@@ -5771,7 +5774,7 @@ var init_network_configs = __esm({
|
|
|
5771
5774
|
});
|
|
5772
5775
|
|
|
5773
5776
|
// src/modules/diamond-hands-sdk.ts
|
|
5774
|
-
import { Contract as Contract9, Interface as Interface5, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2565, solidityPackedKeccak256 as solidityPackedKeccak2564, toUtf8Bytes as toUtf8Bytes5, getBytes as getBytes4, zeroPadValue as zeroPadValue5, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3
|
|
5777
|
+
import { Contract as Contract9, Interface as Interface5, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2565, solidityPackedKeccak256 as solidityPackedKeccak2564, toUtf8Bytes as toUtf8Bytes5, getBytes as getBytes4, zeroPadValue as zeroPadValue5, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3 } from "ethers";
|
|
5775
5778
|
|
|
5776
5779
|
// src/types/result.ts
|
|
5777
5780
|
function success(value) {
|
|
@@ -6774,6 +6777,41 @@ async function resolveAuthorizationInput(provider, signer, ctx) {
|
|
|
6774
6777
|
return { timestamp: result.timestamp, signature: result.signature };
|
|
6775
6778
|
}
|
|
6776
6779
|
|
|
6780
|
+
// src/utils/loan-helpers.utils.ts
|
|
6781
|
+
function baseMintFeeWei(mintAmountWei, originationFeeBps) {
|
|
6782
|
+
if (mintAmountWei < 0n)
|
|
6783
|
+
throw new Error("baseMintFeeWei: mintAmountWei cannot be negative");
|
|
6784
|
+
if (!Number.isInteger(originationFeeBps) || originationFeeBps < 0 || originationFeeBps > 1e4) {
|
|
6785
|
+
throw new Error(
|
|
6786
|
+
`baseMintFeeWei: originationFeeBps must be an integer in [0, 10000] (got ${String(originationFeeBps)})`
|
|
6787
|
+
);
|
|
6788
|
+
}
|
|
6789
|
+
return mintAmountWei * BigInt(originationFeeBps) / 10000n;
|
|
6790
|
+
}
|
|
6791
|
+
function debtAfterMintExceedsLoanCap(params) {
|
|
6792
|
+
const { currentDebtWei, mintAmountWei, mintFeeWei, maxLoanWei } = params;
|
|
6793
|
+
for (const [name, v] of Object.entries(params)) {
|
|
6794
|
+
if (v < 0n)
|
|
6795
|
+
throw new Error(`debtAfterMintExceedsLoanCap: ${name} cannot be negative`);
|
|
6796
|
+
}
|
|
6797
|
+
return currentDebtWei + mintAmountWei + mintFeeWei > maxLoanWei;
|
|
6798
|
+
}
|
|
6799
|
+
function maxPrincipalWithinLoanCap(params) {
|
|
6800
|
+
const { maxLoanWei, currentDebtWei, originationFeeBps } = params;
|
|
6801
|
+
if (maxLoanWei < 0n || currentDebtWei < 0n) {
|
|
6802
|
+
throw new Error("maxPrincipalWithinLoanCap: amounts cannot be negative");
|
|
6803
|
+
}
|
|
6804
|
+
if (!Number.isInteger(originationFeeBps) || originationFeeBps < 0 || originationFeeBps > 1e4) {
|
|
6805
|
+
throw new Error(
|
|
6806
|
+
`maxPrincipalWithinLoanCap: originationFeeBps must be an integer in [0, 10000] (got ${String(originationFeeBps)})`
|
|
6807
|
+
);
|
|
6808
|
+
}
|
|
6809
|
+
const room = maxLoanWei - currentDebtWei;
|
|
6810
|
+
if (room <= 0n)
|
|
6811
|
+
return 0n;
|
|
6812
|
+
return room * 10000n / (10000n + BigInt(originationFeeBps));
|
|
6813
|
+
}
|
|
6814
|
+
|
|
6777
6815
|
// src/utils/eip712-login.ts
|
|
6778
6816
|
import { hexlify, randomBytes } from "ethers";
|
|
6779
6817
|
function buildLoginDomain(chainId) {
|
|
@@ -8223,23 +8261,37 @@ var BitcoinUtils = class {
|
|
|
8223
8261
|
};
|
|
8224
8262
|
|
|
8225
8263
|
// src/utils/address-conversion.utils.ts
|
|
8226
|
-
function
|
|
8264
|
+
function networksOfValidAddress(address) {
|
|
8265
|
+
const lower2 = address.toLowerCase();
|
|
8266
|
+
if (lower2.startsWith("bcrt1"))
|
|
8267
|
+
return ["regtest"];
|
|
8268
|
+
if (lower2.startsWith("bc1"))
|
|
8269
|
+
return ["mainnet"];
|
|
8270
|
+
if (lower2.startsWith("tb1"))
|
|
8271
|
+
return ["testnet"];
|
|
8272
|
+
if (address.startsWith("1") || address.startsWith("3"))
|
|
8273
|
+
return ["mainnet"];
|
|
8274
|
+
return ["testnet", "regtest"];
|
|
8275
|
+
}
|
|
8276
|
+
function safeValidateBitcoinAddress(address, network) {
|
|
8227
8277
|
if (!address || typeof address !== "string") {
|
|
8228
8278
|
throw new Error(`Invalid Bitcoin address: must be a non-empty string`);
|
|
8229
8279
|
}
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8280
|
+
if (network !== "mainnet" && network !== "testnet" && network !== "regtest") {
|
|
8281
|
+
throw new Error(
|
|
8282
|
+
`Bitcoin network is required to validate an address (got ${JSON.stringify(network)})`
|
|
8283
|
+
);
|
|
8234
8284
|
}
|
|
8235
|
-
if (
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
}
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8285
|
+
if (!BitcoinUtils.validateAddress(address)) {
|
|
8286
|
+
throw new Error(
|
|
8287
|
+
`Invalid Bitcoin address: ${address} (malformed or failed its checksum \u2014 check for a typo)`
|
|
8288
|
+
);
|
|
8289
|
+
}
|
|
8290
|
+
const belongsTo = networksOfValidAddress(address);
|
|
8291
|
+
if (!belongsTo.includes(network)) {
|
|
8292
|
+
throw new Error(
|
|
8293
|
+
`Bitcoin address ${address} is not a ${network} address \u2014 it is a ${belongsTo.join("/")} address. Sending across networks would lose the funds.`
|
|
8294
|
+
);
|
|
8243
8295
|
}
|
|
8244
8296
|
return address;
|
|
8245
8297
|
}
|
|
@@ -10382,7 +10434,102 @@ var LoanStatus = /* @__PURE__ */ ((LoanStatus2) => {
|
|
|
10382
10434
|
})(LoanStatus || {});
|
|
10383
10435
|
|
|
10384
10436
|
// src/modules/loan/loan-query.module.ts
|
|
10385
|
-
import { Contract as Contract4 } from "ethers";
|
|
10437
|
+
import { Contract as Contract4, isAddress as isAddress2 } from "ethers";
|
|
10438
|
+
|
|
10439
|
+
// src/constants/chunks/sdk-limits.ts
|
|
10440
|
+
var THE_GRAPH_MAX_BATCH_SIZE = 1e3;
|
|
10441
|
+
|
|
10442
|
+
// src/utils/borrower-ucd-debt-summary.ts
|
|
10443
|
+
var BORROWER_DEBT_ROWS_PAGE_SIZE = THE_GRAPH_MAX_BATCH_SIZE;
|
|
10444
|
+
var BORROWER_DEBT_ROWS_MAX_PAGES = 10;
|
|
10445
|
+
async function collectBorrowerDebtRows(fetchPage) {
|
|
10446
|
+
const pageSize = BORROWER_DEBT_ROWS_PAGE_SIZE;
|
|
10447
|
+
const rows = [];
|
|
10448
|
+
for (let page = 0; page < BORROWER_DEBT_ROWS_MAX_PAGES; page++) {
|
|
10449
|
+
const batch = await fetchPage(page * pageSize, pageSize);
|
|
10450
|
+
if (batch.length > pageSize) {
|
|
10451
|
+
throw new Error(
|
|
10452
|
+
`Borrower debt page ${page} returned ${batch.length} rows for first: ${pageSize}`
|
|
10453
|
+
);
|
|
10454
|
+
}
|
|
10455
|
+
rows.push(...batch);
|
|
10456
|
+
if (batch.length < pageSize) {
|
|
10457
|
+
return rows;
|
|
10458
|
+
}
|
|
10459
|
+
}
|
|
10460
|
+
throw new Error(
|
|
10461
|
+
`Borrower debt rows exceeded ${BORROWER_DEBT_ROWS_MAX_PAGES} pages of ${pageSize} (${rows.length} rows so far); refusing to return a partial total`
|
|
10462
|
+
);
|
|
10463
|
+
}
|
|
10464
|
+
function parseSubgraphLoanStatus(label) {
|
|
10465
|
+
if (typeof label !== "string") {
|
|
10466
|
+
throw new Error(`Subgraph loan status is not a string: ${String(label)}`);
|
|
10467
|
+
}
|
|
10468
|
+
const value = LoanStatus[label];
|
|
10469
|
+
if (typeof value !== "number") {
|
|
10470
|
+
throw new Error(`Unknown subgraph loan status label: "${label}"`);
|
|
10471
|
+
}
|
|
10472
|
+
return value;
|
|
10473
|
+
}
|
|
10474
|
+
function parseSubgraphWei(row) {
|
|
10475
|
+
const raw = row.ucdDebt || "0";
|
|
10476
|
+
if (typeof raw !== "string") {
|
|
10477
|
+
throw new Error(
|
|
10478
|
+
`ucdDebt on position ${row.id} is not a string: ${String(raw)}`
|
|
10479
|
+
);
|
|
10480
|
+
}
|
|
10481
|
+
let wei;
|
|
10482
|
+
try {
|
|
10483
|
+
wei = BigInt(raw);
|
|
10484
|
+
} catch (error) {
|
|
10485
|
+
throw new Error(
|
|
10486
|
+
`ucdDebt on position ${row.id} is not an integer wei string: "${raw}"`,
|
|
10487
|
+
{ cause: error }
|
|
10488
|
+
);
|
|
10489
|
+
}
|
|
10490
|
+
if (wei < 0n) {
|
|
10491
|
+
throw new Error(`Negative ucdDebt on position ${row.id}: ${raw}`);
|
|
10492
|
+
}
|
|
10493
|
+
return wei;
|
|
10494
|
+
}
|
|
10495
|
+
var WEI_PER_UCD = 10n ** 18n;
|
|
10496
|
+
function weiToHumanUcdString(wei) {
|
|
10497
|
+
if (wei < 0n) {
|
|
10498
|
+
throw new Error(`Cannot render negative wei: ${wei}`);
|
|
10499
|
+
}
|
|
10500
|
+
const whole = wei / WEI_PER_UCD;
|
|
10501
|
+
const frac = (wei % WEI_PER_UCD).toString().padStart(18, "0").replace(/0+$/, "");
|
|
10502
|
+
return frac.length > 0 ? `${whole}.${frac}` : `${whole}`;
|
|
10503
|
+
}
|
|
10504
|
+
var STATUS_ORDINALS = Object.values(LoanStatus).filter((v) => typeof v === "number").sort((a, b) => a - b);
|
|
10505
|
+
function buildBorrowerUcdDebtSummary(borrower, rows, fetchedAt = Date.now()) {
|
|
10506
|
+
let totalWei = 0n;
|
|
10507
|
+
const counts = /* @__PURE__ */ new Map();
|
|
10508
|
+
for (const row of rows) {
|
|
10509
|
+
totalWei += parseSubgraphWei(row);
|
|
10510
|
+
const status = parseSubgraphLoanStatus(row.status);
|
|
10511
|
+
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
10512
|
+
}
|
|
10513
|
+
const byStatus = [];
|
|
10514
|
+
for (const status of STATUS_ORDINALS) {
|
|
10515
|
+
const count = counts.get(status);
|
|
10516
|
+
if (count === void 0) {
|
|
10517
|
+
continue;
|
|
10518
|
+
}
|
|
10519
|
+
byStatus.push({ status, statusLabel: LoanStatus[status], count });
|
|
10520
|
+
}
|
|
10521
|
+
return {
|
|
10522
|
+
borrower: borrower.toLowerCase(),
|
|
10523
|
+
loanCount: rows.length,
|
|
10524
|
+
totalUcdDebt: totalWei.toString(),
|
|
10525
|
+
totalUcdDebtHuman: weiToHumanUcdString(totalWei),
|
|
10526
|
+
byStatus,
|
|
10527
|
+
source: "subgraph",
|
|
10528
|
+
fetchedAt
|
|
10529
|
+
};
|
|
10530
|
+
}
|
|
10531
|
+
|
|
10532
|
+
// src/modules/loan/loan-query.module.ts
|
|
10386
10533
|
var POSITION_CORE_ABI = [
|
|
10387
10534
|
"function getPositionDetails(bytes32) view returns (tuple(bytes32 positionId, bytes32 pkpId, uint256 ucdDebt, string vaultAddress, address borrower, uint40 createdAt, uint40 lastUpdated, uint16 selectedTerm, uint40 expiryAt, uint8 status, uint40 previousExpiryAt, uint16 totalTerm))"
|
|
10388
10535
|
];
|
|
@@ -10775,6 +10922,43 @@ var LoanQuery = class {
|
|
|
10775
10922
|
async getLoansByBorrower(borrower, pagination, orderBy, orderDirection) {
|
|
10776
10923
|
return this.getLoans({ borrower, orderBy, orderDirection }, pagination);
|
|
10777
10924
|
}
|
|
10925
|
+
/**
|
|
10926
|
+
* Combined UCD debt + by-status counts for a borrower from ONE pass over the
|
|
10927
|
+
* subgraph's raw rows (`graphClient.getBorrowerDebtRows`): BigInt wei sum, strict
|
|
10928
|
+
* status labels, `source: "subgraph"`.
|
|
10929
|
+
*
|
|
10930
|
+
* INDEXED figure, not a transaction input — the subgraph lags writes. Anything that
|
|
10931
|
+
* sizes a repayment reads the chain (`getPositionDetailsView`). Fails loud: a page
|
|
10932
|
+
* failure or the page cap is a SUBGRAPH-category failure carrying the cause; there
|
|
10933
|
+
* is no partial total.
|
|
10934
|
+
*/
|
|
10935
|
+
async getBorrowerUcdDebtSummary(borrower) {
|
|
10936
|
+
const trimmed = typeof borrower === "string" ? borrower.trim() : "";
|
|
10937
|
+
if (!isAddress2(trimmed)) {
|
|
10938
|
+
return failure(
|
|
10939
|
+
new SDKError({
|
|
10940
|
+
message: `Borrower must be an EVM address for a UCD debt summary, got "${trimmed}"`,
|
|
10941
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
10942
|
+
severity: "LOW" /* LOW */,
|
|
10943
|
+
context: { borrower: trimmed }
|
|
10944
|
+
})
|
|
10945
|
+
);
|
|
10946
|
+
}
|
|
10947
|
+
return tryCatchAsync(
|
|
10948
|
+
async () => {
|
|
10949
|
+
const rows = await this.config.graphClient.getBorrowerDebtRows(trimmed);
|
|
10950
|
+
return buildBorrowerUcdDebtSummary(trimmed, rows);
|
|
10951
|
+
},
|
|
10952
|
+
(error) => error instanceof SDKError ? error : new SDKError({
|
|
10953
|
+
message: `Borrower UCD debt summary failed for ${trimmed}: ${error instanceof Error ? error.message : String(error)}`,
|
|
10954
|
+
category: "SUBGRAPH" /* SUBGRAPH */,
|
|
10955
|
+
severity: "MEDIUM" /* MEDIUM */,
|
|
10956
|
+
originalError: error instanceof Error ? error : void 0,
|
|
10957
|
+
cause: error,
|
|
10958
|
+
context: { borrower: trimmed }
|
|
10959
|
+
})
|
|
10960
|
+
);
|
|
10961
|
+
}
|
|
10778
10962
|
/**
|
|
10779
10963
|
* Get active loans (status = ACTIVE)
|
|
10780
10964
|
*
|
|
@@ -12309,10 +12493,36 @@ var GraphClient = class {
|
|
|
12309
12493
|
}
|
|
12310
12494
|
};
|
|
12311
12495
|
|
|
12312
|
-
// src/constants/chunks/sdk-limits.ts
|
|
12313
|
-
var THE_GRAPH_MAX_BATCH_SIZE = 1e3;
|
|
12314
|
-
|
|
12315
12496
|
// src/graphs/diamond-hands.ts
|
|
12497
|
+
var GET_USER_POSITIONS_DOCUMENT = `
|
|
12498
|
+
query GetUserPositions($borrower: Bytes!, $first: Int, $skip: Int, $orderBy: Position_orderBy, $orderDirection: OrderDirection) {
|
|
12499
|
+
user(id: $borrower) {
|
|
12500
|
+
id
|
|
12501
|
+
positions(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection) {
|
|
12502
|
+
id
|
|
12503
|
+
pkpId
|
|
12504
|
+
borrower {
|
|
12505
|
+
createdAt
|
|
12506
|
+
id
|
|
12507
|
+
}
|
|
12508
|
+
# btcAmount removed - balance is now queried on-chain via lit-actions with signature validation
|
|
12509
|
+
ucdMinted
|
|
12510
|
+
ucdPaid
|
|
12511
|
+
ucdDebt
|
|
12512
|
+
collateralRatio
|
|
12513
|
+
requestedCollateralRatio
|
|
12514
|
+
selectedTerm
|
|
12515
|
+
status
|
|
12516
|
+
createdAt
|
|
12517
|
+
createdAtBlock
|
|
12518
|
+
lastUpdated
|
|
12519
|
+
originalTerm
|
|
12520
|
+
remainingDebt
|
|
12521
|
+
expiryAt
|
|
12522
|
+
}
|
|
12523
|
+
}
|
|
12524
|
+
}
|
|
12525
|
+
`;
|
|
12316
12526
|
function normalizePkpId2(pkpId) {
|
|
12317
12527
|
const raw = pkpId.startsWith("0x") ? pkpId.slice(2) : pkpId;
|
|
12318
12528
|
if (raw.length === 64 && raw.startsWith("000000000000000000000000") && raw !== "0".repeat(64)) {
|
|
@@ -12630,35 +12840,7 @@ var DiamondHandsGraph = class {
|
|
|
12630
12840
|
* Get user positions from subgraph
|
|
12631
12841
|
*/
|
|
12632
12842
|
async getUserPositionsOnly(userAddress, first, skip, orderBy, orderDirection) {
|
|
12633
|
-
const query =
|
|
12634
|
-
query GetUserPositions($borrower: Bytes!, $first: Int, $skip: Int, $orderBy: Position_orderBy, $orderDirection: OrderDirection) {
|
|
12635
|
-
user(id: $borrower) {
|
|
12636
|
-
id
|
|
12637
|
-
positions(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection) {
|
|
12638
|
-
id
|
|
12639
|
-
pkpId
|
|
12640
|
-
borrower {
|
|
12641
|
-
createdAt
|
|
12642
|
-
id
|
|
12643
|
-
}
|
|
12644
|
-
# btcAmount removed - balance is now queried on-chain via lit-actions with signature validation
|
|
12645
|
-
ucdMinted
|
|
12646
|
-
ucdPaid
|
|
12647
|
-
ucdDebt
|
|
12648
|
-
collateralRatio
|
|
12649
|
-
requestedCollateralRatio
|
|
12650
|
-
selectedTerm
|
|
12651
|
-
status
|
|
12652
|
-
createdAt
|
|
12653
|
-
createdAtBlock
|
|
12654
|
-
lastUpdated
|
|
12655
|
-
originalTerm
|
|
12656
|
-
remainingDebt
|
|
12657
|
-
expiryAt
|
|
12658
|
-
}
|
|
12659
|
-
}
|
|
12660
|
-
}
|
|
12661
|
-
`;
|
|
12843
|
+
const query = GET_USER_POSITIONS_DOCUMENT;
|
|
12662
12844
|
const variables = {
|
|
12663
12845
|
borrower: userAddress.toLowerCase(),
|
|
12664
12846
|
first: first || void 0,
|
|
@@ -12676,6 +12858,45 @@ var DiamondHandsGraph = class {
|
|
|
12676
12858
|
}
|
|
12677
12859
|
return positions;
|
|
12678
12860
|
}
|
|
12861
|
+
/**
|
|
12862
|
+
* Raw `{ id, status, ucdDebt }` for EVERY position of a borrower: one round trip per
|
|
12863
|
+
* 1000 positions, no count walk, no vault RPCs, and no `LoanData` transform (which
|
|
12864
|
+
* coerces wei to a float and leaves `status` a string cast to the numeric enum).
|
|
12865
|
+
*
|
|
12866
|
+
* Deliberately issues the `GetUserPositions` document rather than a leaner one: the
|
|
12867
|
+
* lit-ops graph proxy only passes allowlisted documents (graph-document-policy.ts),
|
|
12868
|
+
* and that one is already listed, so this needs no server change. The extra fields
|
|
12869
|
+
* cost bytes, not round trips.
|
|
12870
|
+
*
|
|
12871
|
+
* Throws past `BORROWER_DEBT_ROWS_MAX_PAGES` — never a partial set.
|
|
12872
|
+
*/
|
|
12873
|
+
async getBorrowerDebtRows(borrower) {
|
|
12874
|
+
const borrowerId = borrower.toLowerCase();
|
|
12875
|
+
return collectBorrowerDebtRows(async (skip, first) => {
|
|
12876
|
+
const result = await this.client.execute(GET_USER_POSITIONS_DOCUMENT, {
|
|
12877
|
+
borrower: borrowerId,
|
|
12878
|
+
first,
|
|
12879
|
+
skip,
|
|
12880
|
+
orderBy: "id",
|
|
12881
|
+
orderDirection: "asc"
|
|
12882
|
+
});
|
|
12883
|
+
if (result?.user == null) {
|
|
12884
|
+
if (skip > 0) {
|
|
12885
|
+
throw new Error(
|
|
12886
|
+
`getBorrowerDebtRows: user ${borrowerId} vanished from the subgraph at skip ${skip}`
|
|
12887
|
+
);
|
|
12888
|
+
}
|
|
12889
|
+
return [];
|
|
12890
|
+
}
|
|
12891
|
+
const positions = result.user.positions;
|
|
12892
|
+
if (!Array.isArray(positions)) {
|
|
12893
|
+
throw new Error(
|
|
12894
|
+
`getBorrowerDebtRows: subgraph returned no positions array for ${borrowerId} at skip ${skip}`
|
|
12895
|
+
);
|
|
12896
|
+
}
|
|
12897
|
+
return positions.filter((p) => p != null);
|
|
12898
|
+
});
|
|
12899
|
+
}
|
|
12679
12900
|
/**
|
|
12680
12901
|
* Get user positions, plus an accurate total count.
|
|
12681
12902
|
*
|
|
@@ -18896,6 +19117,625 @@ async function mintAgentPkp(params) {
|
|
|
18896
19117
|
clearTimeout(timeout);
|
|
18897
19118
|
}
|
|
18898
19119
|
}
|
|
19120
|
+
var AGENT_STATUS = { None: 0, Active: 1, Revoked: 2 };
|
|
19121
|
+
function planAgentBinding(params) {
|
|
19122
|
+
const { isActive, record, nowSeconds } = params;
|
|
19123
|
+
if (isActive)
|
|
19124
|
+
return { kind: "reuse", agent: record.agent };
|
|
19125
|
+
const status = Number(record.status);
|
|
19126
|
+
if (status === AGENT_STATUS.None || status === AGENT_STATUS.Revoked) {
|
|
19127
|
+
return { kind: "register" };
|
|
19128
|
+
}
|
|
19129
|
+
if (status !== AGENT_STATUS.Active) {
|
|
19130
|
+
throw new Error(`Unknown agent status ${status} in AgentDelegationRegistry`);
|
|
19131
|
+
}
|
|
19132
|
+
if (Number(record.validUntil) > nowSeconds) {
|
|
19133
|
+
throw new Error(
|
|
19134
|
+
"AgentDelegationRegistry reports an unexpired agent as inactive \u2014 the registry is paused. Agent delegation cannot be changed until it is unpaused."
|
|
19135
|
+
);
|
|
19136
|
+
}
|
|
19137
|
+
return { kind: "rotate" };
|
|
19138
|
+
}
|
|
19139
|
+
|
|
19140
|
+
// src/utils/assert-provider-chain.ts
|
|
19141
|
+
async function describeProviderChainMismatch(provider, chainId) {
|
|
19142
|
+
const actual = Number((await provider.getNetwork()).chainId);
|
|
19143
|
+
if (actual === chainId)
|
|
19144
|
+
return null;
|
|
19145
|
+
return `Network mismatch: the SDK was configured for chainId ${chainId}, but the provider is connected to chainId ${actual}. Switch the wallet/RPC to chainId ${chainId}, or configure the SDK for chainId ${actual}. Nothing was signed.`;
|
|
19146
|
+
}
|
|
19147
|
+
|
|
19148
|
+
// src/utils/sign-guard/psm-exchange.ts
|
|
19149
|
+
import { ethers as ethers5 } from "ethers";
|
|
19150
|
+
|
|
19151
|
+
// src/utils/sign-guard/errors.ts
|
|
19152
|
+
var TxValidationError = class extends Error {
|
|
19153
|
+
/**
|
|
19154
|
+
* @param subject What is being refused. Defaults to the server-returned transaction the
|
|
19155
|
+
* validator exists for; client-built flows (the PSM exchange) name themselves instead.
|
|
19156
|
+
*/
|
|
19157
|
+
constructor(msg, subject = "server-returned transaction") {
|
|
19158
|
+
super(`Refusing to sign ${subject}: ${msg}`);
|
|
19159
|
+
this.name = "TxValidationError";
|
|
19160
|
+
}
|
|
19161
|
+
};
|
|
19162
|
+
|
|
19163
|
+
// src/utils/sign-guard/signable-functions.ts
|
|
19164
|
+
import { ethers as ethers2 } from "ethers";
|
|
19165
|
+
var SIGNABLE_FUNCTIONS = [
|
|
19166
|
+
"function mintUCD(bytes32 positionId, uint256 mintAmount, uint256 mintFee, uint256 newDebt, uint256 newCollateral, uint256 btcPrice, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractHash, uint256 quantumTimestamp, bytes calldata mintValidatorSignature) external returns (bool)",
|
|
19167
|
+
// `makePayment` is the ONLY repayment entry point. A `repayPosition` entry
|
|
19168
|
+
// used to sit here; it is not a function on any deployed contract — the
|
|
19169
|
+
// compiled PositionManager ABI has no `repay*` function at all, and the name
|
|
19170
|
+
// survives only in `archive/` prototypes. Nothing in lit-ops-server, the
|
|
19171
|
+
// lit-actions, or the SDK ever emitted it, and AgentModule never whitelisted
|
|
19172
|
+
// it. A signable-function whitelist should not carry calldata shapes the
|
|
19173
|
+
// protocol cannot execute.
|
|
19174
|
+
"function makePayment(bytes32 positionId, uint256 paymentAmount, uint256 quantumTimestamp, uint256 btcPrice, bytes calldata paymentValidatorSignature) external returns (bool)",
|
|
19175
|
+
"function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, uint256 proRataRenewalFee, bytes calldata extensionValidatorSignature) external returns (bool)",
|
|
19176
|
+
"function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, bytes calldata extensionValidatorSignature) external returns (bool)",
|
|
19177
|
+
// Struct-version: matches the actual on-chain function signature and the
|
|
19178
|
+
// SDK's calldata builder. An earlier 3-arg declaration was stale and would
|
|
19179
|
+
// silently fail to decode real `withdrawBTC` calldata. networkFee is NOT a
|
|
19180
|
+
// contract param — it is an off-chain Phase-2 BTC tx fee, validated elsewhere.
|
|
19181
|
+
"function withdrawBTC((bytes32 positionId, bytes32 actionHash, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractBundleHash, string withdrawalAddress, uint256 totalDeduction, uint256 newCollateral, uint256 quantumTimestamp, uint256 btcPrice, string utxoTxid, uint32 utxoVout) params, bytes withdrawalValidatorSignature, bytes btcSpendAuthSignature) external returns (bool)",
|
|
19182
|
+
// `cancelStaleSpendByOwner` removed in audit #62210 follow-up — the on-chain
|
|
19183
|
+
// function reverts permanently. The trustless replacement
|
|
19184
|
+
// (`cancelStaleSpendWithProof` on BTCSpendAuthorizer) lands via a separate
|
|
19185
|
+
// entry when its LIT Action ships.
|
|
19186
|
+
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
19187
|
+
"function setPositionDelegate(bytes32 positionId, address newDelegate) external",
|
|
19188
|
+
// Position creation. Signed opaquely by the SDK (like withdrawBTC), so
|
|
19189
|
+
// without an entry here it decoded as "<unparseable>" and the CLI's
|
|
19190
|
+
// WithdrawGuard waved it through — the flow that locks the user's BTC
|
|
19191
|
+
// collateral had no client-side pin at all.
|
|
19192
|
+
"function createPosition(bytes32 pkpId, bytes calldata validatorSignature, string mainnetVaultAddress, string regtestVaultAddress, uint256 selectedTermMonths, uint256 validatorVersion, bytes calldata pkpPublicKey) external returns (bytes32 positionId)",
|
|
19193
|
+
// SimplePSMV2 — the PSM exchange pair. Direct EOA calls, never routed through
|
|
19194
|
+
// AgentModule; the parity test documents them as client-only.
|
|
19195
|
+
"function swap(address stablecoin, uint256 amountIn, uint256 minUcdOut) external returns (uint256)",
|
|
19196
|
+
"function redeem(address stablecoin, uint256 ucdAmount, uint256 minStablecoinOut) external returns (uint256)",
|
|
19197
|
+
// BitcoinWithdrawalAddressRegistry — `msg.sender` must be the borrower, so
|
|
19198
|
+
// this too is client-only and never module-whitelisted.
|
|
19199
|
+
"function addAddress(string btcAddress) external"
|
|
19200
|
+
];
|
|
19201
|
+
var IFACE = new ethers2.Interface(SIGNABLE_FUNCTIONS);
|
|
19202
|
+
function encodeSignable(fn, args) {
|
|
19203
|
+
return IFACE.encodeFunctionData(fn, args);
|
|
19204
|
+
}
|
|
19205
|
+
function decodeSignable(data) {
|
|
19206
|
+
try {
|
|
19207
|
+
const parsed = IFACE.parseTransaction({ data });
|
|
19208
|
+
if (!parsed) {
|
|
19209
|
+
throw new TxValidationError(
|
|
19210
|
+
`unsignedTx.data does not decode against the signable-function whitelist`
|
|
19211
|
+
);
|
|
19212
|
+
}
|
|
19213
|
+
return { name: parsed.name, args: parsed.args };
|
|
19214
|
+
} catch (e) {
|
|
19215
|
+
if (e instanceof TxValidationError)
|
|
19216
|
+
throw e;
|
|
19217
|
+
throw new TxValidationError(
|
|
19218
|
+
`unsignedTx.data does not decode against the signable-function whitelist: ${e.message}`
|
|
19219
|
+
);
|
|
19220
|
+
}
|
|
19221
|
+
}
|
|
19222
|
+
|
|
19223
|
+
// src/utils/sign-guard/tx-validator.ts
|
|
19224
|
+
import { ethers as ethers4 } from "ethers";
|
|
19225
|
+
|
|
19226
|
+
// src/utils/sign-guard/fee-ceiling.ts
|
|
19227
|
+
import { ethers as ethers3 } from "ethers";
|
|
19228
|
+
var MAX_GAS_LIMIT = 5000000n;
|
|
19229
|
+
var MAX_FEE_PER_GAS_WEI = 2000000000000n;
|
|
19230
|
+
var DEFAULT_MAX_TX_FEE_WEI = 250000000000000000n;
|
|
19231
|
+
var DEFAULT_FEE_CEILING = {
|
|
19232
|
+
maxGasLimit: MAX_GAS_LIMIT,
|
|
19233
|
+
maxFeePerGasWei: MAX_FEE_PER_GAS_WEI,
|
|
19234
|
+
maxTxFeeWei: DEFAULT_MAX_TX_FEE_WEI
|
|
19235
|
+
};
|
|
19236
|
+
function fmtEth(wei) {
|
|
19237
|
+
return `${ethers3.formatEther(wei)} ETH`;
|
|
19238
|
+
}
|
|
19239
|
+
function fmtGwei(wei) {
|
|
19240
|
+
return `${ethers3.formatUnits(wei, "gwei")} gwei`;
|
|
19241
|
+
}
|
|
19242
|
+
function worstCaseFeeWei(gasLimit, maxFeePerGas) {
|
|
19243
|
+
if (gasLimit == null || maxFeePerGas == null)
|
|
19244
|
+
return null;
|
|
19245
|
+
return gasLimit * maxFeePerGas;
|
|
19246
|
+
}
|
|
19247
|
+
function assertFeeCeiling(gasLimit, maxFeePerGas, raise, ceiling = DEFAULT_FEE_CEILING) {
|
|
19248
|
+
if (gasLimit != null && gasLimit > ceiling.maxGasLimit) {
|
|
19249
|
+
raise(`gasLimit ${gasLimit} exceeds the signing ceiling of ${ceiling.maxGasLimit}`);
|
|
19250
|
+
}
|
|
19251
|
+
if (maxFeePerGas != null && maxFeePerGas > ceiling.maxFeePerGasWei) {
|
|
19252
|
+
raise(
|
|
19253
|
+
`maxFeePerGas ${fmtGwei(maxFeePerGas)} exceeds the signing ceiling of ${fmtGwei(ceiling.maxFeePerGasWei)}`
|
|
19254
|
+
);
|
|
19255
|
+
}
|
|
19256
|
+
const worst = worstCaseFeeWei(gasLimit, maxFeePerGas);
|
|
19257
|
+
if (worst != null && worst > ceiling.maxTxFeeWei) {
|
|
19258
|
+
raise(
|
|
19259
|
+
`worst-case transaction fee ${fmtEth(worst)} (gasLimit ${gasLimit} \xD7 ${fmtGwei(maxFeePerGas)}) exceeds the signing ceiling of ${fmtEth(ceiling.maxTxFeeWei)}.${ceiling.raiseHint ? ` ${ceiling.raiseHint}` : ""}`
|
|
19260
|
+
);
|
|
19261
|
+
}
|
|
19262
|
+
}
|
|
19263
|
+
|
|
19264
|
+
// src/utils/sign-guard/tx-validator.ts
|
|
19265
|
+
function eqAddr(a, b) {
|
|
19266
|
+
if (!a || !b)
|
|
19267
|
+
return false;
|
|
19268
|
+
try {
|
|
19269
|
+
return ethers4.getAddress(a) === ethers4.getAddress(b);
|
|
19270
|
+
} catch {
|
|
19271
|
+
return false;
|
|
19272
|
+
}
|
|
19273
|
+
}
|
|
19274
|
+
function readToAddress(tx) {
|
|
19275
|
+
const to = tx["to"];
|
|
19276
|
+
if (typeof to !== "string") {
|
|
19277
|
+
throw new TxValidationError(`unsignedTx.to is missing or not a string`);
|
|
19278
|
+
}
|
|
19279
|
+
return to;
|
|
19280
|
+
}
|
|
19281
|
+
function readData(tx) {
|
|
19282
|
+
const data = tx["data"];
|
|
19283
|
+
if (typeof data !== "string" || !data.startsWith("0x")) {
|
|
19284
|
+
throw new TxValidationError(`unsignedTx.data missing or malformed`);
|
|
19285
|
+
}
|
|
19286
|
+
return data;
|
|
19287
|
+
}
|
|
19288
|
+
function readChainId(tx) {
|
|
19289
|
+
const c = tx["chainId"];
|
|
19290
|
+
if (c == null)
|
|
19291
|
+
return null;
|
|
19292
|
+
if (typeof c === "number")
|
|
19293
|
+
return c;
|
|
19294
|
+
if (typeof c === "string")
|
|
19295
|
+
return parseInt(c.startsWith("0x") ? c.slice(2) : c, c.startsWith("0x") ? 16 : 10);
|
|
19296
|
+
if (typeof c === "bigint")
|
|
19297
|
+
return Number(c);
|
|
19298
|
+
return null;
|
|
19299
|
+
}
|
|
19300
|
+
function readOptionalBigInt(tx, key) {
|
|
19301
|
+
const v = tx[key];
|
|
19302
|
+
if (v == null)
|
|
19303
|
+
return null;
|
|
19304
|
+
try {
|
|
19305
|
+
if (typeof v === "bigint")
|
|
19306
|
+
return v;
|
|
19307
|
+
if (typeof v === "number")
|
|
19308
|
+
return BigInt(v);
|
|
19309
|
+
if (typeof v === "string")
|
|
19310
|
+
return BigInt(v);
|
|
19311
|
+
return BigInt(v.toString());
|
|
19312
|
+
} catch {
|
|
19313
|
+
throw new TxValidationError(`unsignedTx.${key} is present but not a valid integer`);
|
|
19314
|
+
}
|
|
19315
|
+
}
|
|
19316
|
+
function readValue(tx) {
|
|
19317
|
+
const v = tx["value"];
|
|
19318
|
+
if (v == null)
|
|
19319
|
+
return 0n;
|
|
19320
|
+
if (typeof v === "string")
|
|
19321
|
+
return BigInt(v);
|
|
19322
|
+
if (typeof v === "number")
|
|
19323
|
+
return BigInt(v);
|
|
19324
|
+
if (typeof v === "bigint")
|
|
19325
|
+
return v;
|
|
19326
|
+
const maybe = v;
|
|
19327
|
+
if (typeof maybe.toBigInt === "function")
|
|
19328
|
+
return maybe.toBigInt();
|
|
19329
|
+
if (typeof maybe.toString === "function")
|
|
19330
|
+
return BigInt(maybe.toString());
|
|
19331
|
+
throw new TxValidationError(`unsignedTx.value has unrecognized type`);
|
|
19332
|
+
}
|
|
19333
|
+
function normalizePositionId(id) {
|
|
19334
|
+
const hex = id.startsWith("0x") ? id : `0x${id}`;
|
|
19335
|
+
return ethers4.zeroPadValue(hex, 32).toLowerCase();
|
|
19336
|
+
}
|
|
19337
|
+
function requirePositionId(actual, expected) {
|
|
19338
|
+
const txPositionId = String(actual).toLowerCase();
|
|
19339
|
+
if (txPositionId !== normalizePositionId(expected)) {
|
|
19340
|
+
throw new TxValidationError(
|
|
19341
|
+
`positionId mismatch \u2014 tx ${txPositionId}, expected ${normalizePositionId(expected)}`
|
|
19342
|
+
);
|
|
19343
|
+
}
|
|
19344
|
+
}
|
|
19345
|
+
function validateUnsignedTx(unsignedTx, expected, vctx) {
|
|
19346
|
+
const txChainId = readChainId(unsignedTx);
|
|
19347
|
+
if (txChainId == null) {
|
|
19348
|
+
throw new TxValidationError(
|
|
19349
|
+
`unsignedTx.chainId is missing \u2014 refusing to sign a transaction with no chain binding (client is on ${vctx.chainId})`
|
|
19350
|
+
);
|
|
19351
|
+
}
|
|
19352
|
+
if (txChainId !== vctx.chainId) {
|
|
19353
|
+
throw new TxValidationError(
|
|
19354
|
+
`chainId mismatch \u2014 tx says ${txChainId}, client is on ${vctx.chainId}`
|
|
19355
|
+
);
|
|
19356
|
+
}
|
|
19357
|
+
const value = readValue(unsignedTx);
|
|
19358
|
+
if (value !== 0n) {
|
|
19359
|
+
throw new TxValidationError(
|
|
19360
|
+
`unsignedTx.value is non-zero (${value}); none of the protocol functions accept ETH`
|
|
19361
|
+
);
|
|
19362
|
+
}
|
|
19363
|
+
assertFeeCeiling(
|
|
19364
|
+
readOptionalBigInt(unsignedTx, "gasLimit"),
|
|
19365
|
+
readOptionalBigInt(unsignedTx, "maxFeePerGas") ?? readOptionalBigInt(unsignedTx, "gasPrice"),
|
|
19366
|
+
(msg) => {
|
|
19367
|
+
throw new TxValidationError(msg);
|
|
19368
|
+
},
|
|
19369
|
+
vctx.feeCeiling
|
|
19370
|
+
);
|
|
19371
|
+
const to = readToAddress(unsignedTx);
|
|
19372
|
+
const data = readData(unsignedTx);
|
|
19373
|
+
const { name, args } = decodeSignable(data);
|
|
19374
|
+
const pm = vctx.contracts.PositionManager;
|
|
19375
|
+
switch (expected.kind) {
|
|
19376
|
+
case "mint": {
|
|
19377
|
+
if (name !== "mintUCD") {
|
|
19378
|
+
throw new TxValidationError(`expected mintUCD, server returned ${name}`);
|
|
19379
|
+
}
|
|
19380
|
+
if (!eqAddr(to, pm)) {
|
|
19381
|
+
throw new TxValidationError(`mintUCD must target PositionManager (${pm}), got ${to}`);
|
|
19382
|
+
}
|
|
19383
|
+
requirePositionId(args[0], expected.positionId);
|
|
19384
|
+
const txMintAmount = BigInt(String(args[1]));
|
|
19385
|
+
if (txMintAmount.toString() !== expected.amountWei) {
|
|
19386
|
+
throw new TxValidationError(`mintAmount mismatch \u2014 tx ${txMintAmount}, expected ${expected.amountWei}`);
|
|
19387
|
+
}
|
|
19388
|
+
const txMintFee = BigInt(String(args[2]));
|
|
19389
|
+
if (txMintFee.toString() !== expected.mintFeeWei) {
|
|
19390
|
+
throw new TxValidationError(`mintFee mismatch \u2014 tx ${txMintFee}, expected ${expected.mintFeeWei}`);
|
|
19391
|
+
}
|
|
19392
|
+
break;
|
|
19393
|
+
}
|
|
19394
|
+
case "repay": {
|
|
19395
|
+
if (name !== "makePayment") {
|
|
19396
|
+
throw new TxValidationError(`expected makePayment, server returned ${name}`);
|
|
19397
|
+
}
|
|
19398
|
+
if (!eqAddr(to, pm)) {
|
|
19399
|
+
throw new TxValidationError(`${name} must target PositionManager (${pm}), got ${to}`);
|
|
19400
|
+
}
|
|
19401
|
+
requirePositionId(args[0], expected.positionId);
|
|
19402
|
+
const txAmount = BigInt(String(args[1]));
|
|
19403
|
+
if (txAmount.toString() !== expected.amountWei) {
|
|
19404
|
+
throw new TxValidationError(`paymentAmount mismatch \u2014 tx ${txAmount}, expected ${expected.amountWei}`);
|
|
19405
|
+
}
|
|
19406
|
+
break;
|
|
19407
|
+
}
|
|
19408
|
+
case "extend": {
|
|
19409
|
+
if (name !== "extendPosition") {
|
|
19410
|
+
throw new TxValidationError(`expected extendPosition, server returned ${name}`);
|
|
19411
|
+
}
|
|
19412
|
+
if (!eqAddr(to, pm)) {
|
|
19413
|
+
throw new TxValidationError(`extendPosition must target PositionManager (${pm}), got ${to}`);
|
|
19414
|
+
}
|
|
19415
|
+
requirePositionId(args[0], expected.positionId);
|
|
19416
|
+
const txTerm = Number(args[1]);
|
|
19417
|
+
if (txTerm !== expected.selectedTerm) {
|
|
19418
|
+
throw new TxValidationError(`selectedTerm mismatch \u2014 tx ${txTerm}, expected ${expected.selectedTerm}`);
|
|
19419
|
+
}
|
|
19420
|
+
if (expected.upperBoundProRataFeeWei != null && args.length >= 7) {
|
|
19421
|
+
const txFee = BigInt(String(args[5]));
|
|
19422
|
+
const upper = BigInt(expected.upperBoundProRataFeeWei);
|
|
19423
|
+
if (txFee > upper) {
|
|
19424
|
+
throw new TxValidationError(
|
|
19425
|
+
`proRataRenewalFee=${txFee} exceeds upper bound=${upper}. A compromised validator could otherwise inflate the fee to drain UCD.`
|
|
19426
|
+
);
|
|
19427
|
+
}
|
|
19428
|
+
}
|
|
19429
|
+
break;
|
|
19430
|
+
}
|
|
19431
|
+
case "withdraw-btc": {
|
|
19432
|
+
if (name !== "withdrawBTC") {
|
|
19433
|
+
throw new TxValidationError(`expected withdrawBTC, server returned ${name}`);
|
|
19434
|
+
}
|
|
19435
|
+
if (!eqAddr(to, pm)) {
|
|
19436
|
+
throw new TxValidationError(`withdrawBTC must target PositionManager (${pm}), got ${to}`);
|
|
19437
|
+
}
|
|
19438
|
+
const paramsTuple = args[0];
|
|
19439
|
+
requirePositionId(paramsTuple[0], expected.positionId);
|
|
19440
|
+
const txWithdrawalAddress = String(paramsTuple[5]);
|
|
19441
|
+
if (txWithdrawalAddress !== expected.btcAddress) {
|
|
19442
|
+
throw new TxValidationError(`btcAddress mismatch \u2014 tx '${txWithdrawalAddress}', expected '${expected.btcAddress}'`);
|
|
19443
|
+
}
|
|
19444
|
+
if (expected.upperBoundTotalDeduction != null) {
|
|
19445
|
+
const txDeduction = BigInt(String(paramsTuple[6]));
|
|
19446
|
+
const upper = BigInt(expected.upperBoundTotalDeduction);
|
|
19447
|
+
if (txDeduction > upper) {
|
|
19448
|
+
throw new TxValidationError(
|
|
19449
|
+
`totalDeduction=${txDeduction} exceeds approved upper bound=${upper}. A compromised validator could otherwise drain collateral beyond the approved amount.`
|
|
19450
|
+
);
|
|
19451
|
+
}
|
|
19452
|
+
}
|
|
19453
|
+
break;
|
|
19454
|
+
}
|
|
19455
|
+
case "ucd-approve": {
|
|
19456
|
+
if (name !== "approve") {
|
|
19457
|
+
throw new TxValidationError(`expected ERC-20 approve, server returned ${name}`);
|
|
19458
|
+
}
|
|
19459
|
+
const ucd = vctx.contracts.UCDToken;
|
|
19460
|
+
if (!eqAddr(to, ucd)) {
|
|
19461
|
+
throw new TxValidationError(`approve must target UCDToken (${ucd}), got ${to}`);
|
|
19462
|
+
}
|
|
19463
|
+
const spender = String(args[0]);
|
|
19464
|
+
const ok = expected.spenderCandidates.some((s) => eqAddr(spender, s));
|
|
19465
|
+
if (!ok) {
|
|
19466
|
+
throw new TxValidationError(
|
|
19467
|
+
`approve spender ${spender} not in expected set ${expected.spenderCandidates.join(", ")}`
|
|
19468
|
+
);
|
|
19469
|
+
}
|
|
19470
|
+
const amt = BigInt(String(args[1]));
|
|
19471
|
+
const min = BigInt(expected.minAmountWei);
|
|
19472
|
+
if (amt < min) {
|
|
19473
|
+
throw new TxValidationError(`approve amount ${amt} is less than required ${min}`);
|
|
19474
|
+
}
|
|
19475
|
+
const SANITY_MAX = min * 1000n;
|
|
19476
|
+
if (amt > SANITY_MAX) {
|
|
19477
|
+
throw new TxValidationError(`approve amount ${amt} exceeds sanity cap ${SANITY_MAX} (1000x requested)`);
|
|
19478
|
+
}
|
|
19479
|
+
break;
|
|
19480
|
+
}
|
|
19481
|
+
case "create-position": {
|
|
19482
|
+
if (name !== "createPosition") {
|
|
19483
|
+
throw new TxValidationError(`expected createPosition, server returned ${name}`);
|
|
19484
|
+
}
|
|
19485
|
+
if (!eqAddr(to, pm)) {
|
|
19486
|
+
throw new TxValidationError(`createPosition must target PositionManager (${pm}), got ${to}`);
|
|
19487
|
+
}
|
|
19488
|
+
const txTerm = BigInt(String(args[4]));
|
|
19489
|
+
if (txTerm !== BigInt(expected.selectedTerm)) {
|
|
19490
|
+
throw new TxValidationError(`selectedTerm mismatch \u2014 tx ${txTerm}, expected ${expected.selectedTerm}`);
|
|
19491
|
+
}
|
|
19492
|
+
break;
|
|
19493
|
+
}
|
|
19494
|
+
case "set-position-delegate": {
|
|
19495
|
+
if (name !== "setPositionDelegate") {
|
|
19496
|
+
throw new TxValidationError(`expected setPositionDelegate, server returned ${name}`);
|
|
19497
|
+
}
|
|
19498
|
+
if (!eqAddr(to, expected.registryAddress)) {
|
|
19499
|
+
throw new TxValidationError(
|
|
19500
|
+
`setPositionDelegate must target PositionDelegateRegistry (${expected.registryAddress}), got ${to}`
|
|
19501
|
+
);
|
|
19502
|
+
}
|
|
19503
|
+
requirePositionId(args[0], expected.positionId);
|
|
19504
|
+
const txDelegate = String(args[1]);
|
|
19505
|
+
if (!eqAddr(txDelegate, expected.delegate)) {
|
|
19506
|
+
throw new TxValidationError(`delegate mismatch \u2014 tx ${txDelegate}, expected ${expected.delegate}`);
|
|
19507
|
+
}
|
|
19508
|
+
break;
|
|
19509
|
+
}
|
|
19510
|
+
case "bwar-add-address": {
|
|
19511
|
+
if (name !== "addAddress") {
|
|
19512
|
+
throw new TxValidationError(`expected addAddress, decoded ${name}`);
|
|
19513
|
+
}
|
|
19514
|
+
if (!eqAddr(to, expected.registryAddress)) {
|
|
19515
|
+
throw new TxValidationError(
|
|
19516
|
+
`addAddress must target the BitcoinWithdrawalAddressRegistry (${expected.registryAddress}), got ${to}`
|
|
19517
|
+
);
|
|
19518
|
+
}
|
|
19519
|
+
if (String(args[0]) !== expected.btcAddress) {
|
|
19520
|
+
throw new TxValidationError(
|
|
19521
|
+
`btcAddress mismatch \u2014 tx says '${String(args[0])}', user asked for '${expected.btcAddress}'`
|
|
19522
|
+
);
|
|
19523
|
+
}
|
|
19524
|
+
break;
|
|
19525
|
+
}
|
|
19526
|
+
case "stablecoin-approve": {
|
|
19527
|
+
if (name !== "approve") {
|
|
19528
|
+
throw new TxValidationError(`expected ERC-20 approve, server returned ${name}`);
|
|
19529
|
+
}
|
|
19530
|
+
if (!eqAddr(to, expected.tokenAddress)) {
|
|
19531
|
+
throw new TxValidationError(`approve must target the stablecoin token (${expected.tokenAddress}), got ${to}`);
|
|
19532
|
+
}
|
|
19533
|
+
if (!eqAddr(String(args[0]), expected.spender)) {
|
|
19534
|
+
throw new TxValidationError(`approve spender must be the PSM (${expected.spender}), got ${String(args[0])}`);
|
|
19535
|
+
}
|
|
19536
|
+
if (BigInt(String(args[1])).toString() !== expected.amountUnits) {
|
|
19537
|
+
throw new TxValidationError(
|
|
19538
|
+
`approve amount must be EXACTLY ${expected.amountUnits} (exact-amount rule), got ${BigInt(String(args[1]))}`
|
|
19539
|
+
);
|
|
19540
|
+
}
|
|
19541
|
+
break;
|
|
19542
|
+
}
|
|
19543
|
+
case "ucd-approve-controller": {
|
|
19544
|
+
if (name !== "approve") {
|
|
19545
|
+
throw new TxValidationError(`expected ERC-20 approve, server returned ${name}`);
|
|
19546
|
+
}
|
|
19547
|
+
if (!eqAddr(to, expected.ucdTokenAddress)) {
|
|
19548
|
+
throw new TxValidationError(`approve must target UCDToken (${expected.ucdTokenAddress}), got ${to}`);
|
|
19549
|
+
}
|
|
19550
|
+
if (!eqAddr(String(args[0]), expected.spender)) {
|
|
19551
|
+
throw new TxValidationError(`approve spender must be the UCDController (${expected.spender}), got ${String(args[0])}`);
|
|
19552
|
+
}
|
|
19553
|
+
if (BigInt(String(args[1])).toString() !== expected.amountWei) {
|
|
19554
|
+
throw new TxValidationError(
|
|
19555
|
+
`approve amount must be EXACTLY ${expected.amountWei} (exact-amount rule), got ${BigInt(String(args[1]))}`
|
|
19556
|
+
);
|
|
19557
|
+
}
|
|
19558
|
+
break;
|
|
19559
|
+
}
|
|
19560
|
+
case "psm-swap": {
|
|
19561
|
+
if (name !== "swap")
|
|
19562
|
+
throw new TxValidationError(`expected swap, server returned ${name}`);
|
|
19563
|
+
if (!eqAddr(to, expected.psmAddress)) {
|
|
19564
|
+
throw new TxValidationError(`swap must target the PSM (${expected.psmAddress}), got ${to}`);
|
|
19565
|
+
}
|
|
19566
|
+
if (!eqAddr(String(args[0]), expected.stablecoin))
|
|
19567
|
+
throw new TxValidationError(`swap stablecoin mismatch`);
|
|
19568
|
+
if (BigInt(String(args[1])).toString() !== expected.amountIn)
|
|
19569
|
+
throw new TxValidationError(`swap amountIn mismatch`);
|
|
19570
|
+
if (BigInt(String(args[2])).toString() !== expected.minOut)
|
|
19571
|
+
throw new TxValidationError(`swap minUcdOut mismatch`);
|
|
19572
|
+
if (BigInt(expected.minOut) === 0n) {
|
|
19573
|
+
throw new TxValidationError(`swap minUcdOut is 0 \u2014 a slippage floor is mandatory (SlippageProtectionRequired)`);
|
|
19574
|
+
}
|
|
19575
|
+
break;
|
|
19576
|
+
}
|
|
19577
|
+
case "psm-redeem": {
|
|
19578
|
+
if (name !== "redeem")
|
|
19579
|
+
throw new TxValidationError(`expected redeem, server returned ${name}`);
|
|
19580
|
+
if (!eqAddr(to, expected.psmAddress)) {
|
|
19581
|
+
throw new TxValidationError(`redeem must target the PSM (${expected.psmAddress}), got ${to}`);
|
|
19582
|
+
}
|
|
19583
|
+
if (!eqAddr(String(args[0]), expected.stablecoin))
|
|
19584
|
+
throw new TxValidationError(`redeem stablecoin mismatch`);
|
|
19585
|
+
if (BigInt(String(args[1])).toString() !== expected.amountUcdWei)
|
|
19586
|
+
throw new TxValidationError(`redeem ucdAmount mismatch`);
|
|
19587
|
+
if (BigInt(String(args[2])).toString() !== expected.minOut)
|
|
19588
|
+
throw new TxValidationError(`redeem minStablecoinOut mismatch`);
|
|
19589
|
+
if (BigInt(expected.minOut) === 0n) {
|
|
19590
|
+
throw new TxValidationError(`redeem minStablecoinOut is 0 \u2014 a slippage floor is mandatory (SlippageProtectionRequired)`);
|
|
19591
|
+
}
|
|
19592
|
+
break;
|
|
19593
|
+
}
|
|
19594
|
+
default: {
|
|
19595
|
+
const _exhaustive = expected;
|
|
19596
|
+
throw new TxValidationError(`unhandled expected action kind: ${JSON.stringify(_exhaustive)}`);
|
|
19597
|
+
}
|
|
19598
|
+
}
|
|
19599
|
+
}
|
|
19600
|
+
|
|
19601
|
+
// src/utils/sign-guard/psm-exchange.ts
|
|
19602
|
+
function buildPsmApproveTx(p) {
|
|
19603
|
+
return {
|
|
19604
|
+
to: p.token,
|
|
19605
|
+
data: encodeSignable("approve", [p.spender, p.amount]),
|
|
19606
|
+
value: "0x0",
|
|
19607
|
+
chainId: p.chainId
|
|
19608
|
+
};
|
|
19609
|
+
}
|
|
19610
|
+
function buildPsmSwapTx(p) {
|
|
19611
|
+
return {
|
|
19612
|
+
to: p.psm,
|
|
19613
|
+
data: encodeSignable("swap", [p.stablecoin, p.amountIn, p.minOut]),
|
|
19614
|
+
value: "0x0",
|
|
19615
|
+
chainId: p.chainId
|
|
19616
|
+
};
|
|
19617
|
+
}
|
|
19618
|
+
function buildPsmRedeemTx(p) {
|
|
19619
|
+
return {
|
|
19620
|
+
to: p.psm,
|
|
19621
|
+
data: encodeSignable("redeem", [p.stablecoin, p.ucdAmount, p.minOut]),
|
|
19622
|
+
value: "0x0",
|
|
19623
|
+
chainId: p.chainId
|
|
19624
|
+
};
|
|
19625
|
+
}
|
|
19626
|
+
var PSM_READS_ABI = ["function supportedStablecoins(address) view returns (bool)"];
|
|
19627
|
+
var ERC20_READS_ABI = ["function allowance(address owner, address spender) view returns (uint256)"];
|
|
19628
|
+
function psmExchangeReadsFromProvider(provider, psmAddress) {
|
|
19629
|
+
const psm = new ethers5.Contract(psmAddress, PSM_READS_ABI, provider);
|
|
19630
|
+
return {
|
|
19631
|
+
isStablecoinSupported: async (stablecoin) => await psm.getFunction("supportedStablecoins")(stablecoin),
|
|
19632
|
+
allowance: async (token, owner, spender) => await new ethers5.Contract(token, ERC20_READS_ABI, provider).getFunction("allowance")(owner, spender)
|
|
19633
|
+
};
|
|
19634
|
+
}
|
|
19635
|
+
var refuse = (direction, msg) => new TxValidationError(msg, `PSM ${direction}`);
|
|
19636
|
+
function requireAddress(direction, label, value) {
|
|
19637
|
+
if (!value || !ethers5.isAddress(value)) {
|
|
19638
|
+
throw refuse(direction, `${label} address is missing or invalid (${String(value)})`);
|
|
19639
|
+
}
|
|
19640
|
+
return ethers5.getAddress(value);
|
|
19641
|
+
}
|
|
19642
|
+
async function planPsmExchange(params) {
|
|
19643
|
+
const { direction, amountIn, minOut, vctx, reads } = params;
|
|
19644
|
+
const chainId = vctx.chainId;
|
|
19645
|
+
if (amountIn <= 0n) {
|
|
19646
|
+
throw refuse(direction, `amount must be greater than zero (got ${amountIn})`);
|
|
19647
|
+
}
|
|
19648
|
+
if (minOut <= 0n) {
|
|
19649
|
+
throw refuse(direction, `minimum-out floor must be greater than zero (got ${minOut}) \u2014 a zero floor is refused on-chain`);
|
|
19650
|
+
}
|
|
19651
|
+
const owner = requireAddress(direction, "owner", params.owner);
|
|
19652
|
+
const psm = requireAddress(direction, "SimplePSMV2", params.addresses.psm);
|
|
19653
|
+
const stablecoin = requireAddress(direction, "stablecoin", params.addresses.stablecoin);
|
|
19654
|
+
const isSwap = direction === "swap";
|
|
19655
|
+
const token = isSwap ? stablecoin : requireAddress(direction, "UCDToken", params.addresses.ucdToken);
|
|
19656
|
+
const spender = isSwap ? psm : requireAddress(direction, "UCDController", params.addresses.ucdController);
|
|
19657
|
+
if (!await reads.isStablecoinSupported(stablecoin)) {
|
|
19658
|
+
throw refuse(direction, `stablecoin ${stablecoin} is not supported by the PSM at ${psm}`);
|
|
19659
|
+
}
|
|
19660
|
+
const approveExpected = (amount) => isSwap ? { kind: "stablecoin-approve", tokenAddress: token, spender, amountUnits: amount.toString() } : { kind: "ucd-approve-controller", ucdTokenAddress: token, spender, amountWei: amount.toString() };
|
|
19661
|
+
const allowanceBefore = await reads.allowance(token, owner, spender);
|
|
19662
|
+
const approvals = [];
|
|
19663
|
+
if (allowanceBefore < amountIn) {
|
|
19664
|
+
if (allowanceBefore > 0n) {
|
|
19665
|
+
approvals.push({
|
|
19666
|
+
step: "reset-approve",
|
|
19667
|
+
tx: buildPsmApproveTx({ token, spender, amount: 0n, chainId }),
|
|
19668
|
+
expected: approveExpected(0n)
|
|
19669
|
+
});
|
|
19670
|
+
}
|
|
19671
|
+
approvals.push({
|
|
19672
|
+
step: "approve",
|
|
19673
|
+
tx: buildPsmApproveTx({ token, spender, amount: amountIn, chainId }),
|
|
19674
|
+
expected: approveExpected(amountIn)
|
|
19675
|
+
});
|
|
19676
|
+
}
|
|
19677
|
+
const exec = isSwap ? {
|
|
19678
|
+
step: "swap",
|
|
19679
|
+
tx: buildPsmSwapTx({ psm, stablecoin, amountIn, minOut, chainId }),
|
|
19680
|
+
expected: { kind: "psm-swap", psmAddress: psm, stablecoin, amountIn: amountIn.toString(), minOut: minOut.toString() }
|
|
19681
|
+
} : {
|
|
19682
|
+
step: "redeem",
|
|
19683
|
+
tx: buildPsmRedeemTx({ psm, stablecoin, ucdAmount: amountIn, minOut, chainId }),
|
|
19684
|
+
expected: {
|
|
19685
|
+
kind: "psm-redeem",
|
|
19686
|
+
psmAddress: psm,
|
|
19687
|
+
stablecoin,
|
|
19688
|
+
amountUcdWei: amountIn.toString(),
|
|
19689
|
+
minOut: minOut.toString()
|
|
19690
|
+
}
|
|
19691
|
+
};
|
|
19692
|
+
for (const leg of [...approvals, exec]) {
|
|
19693
|
+
validateUnsignedTx({ ...leg.tx }, leg.expected, vctx);
|
|
19694
|
+
}
|
|
19695
|
+
return { direction, owner, token, spender, amountIn, minOut, allowanceBefore, approvals, exec };
|
|
19696
|
+
}
|
|
19697
|
+
async function executePsmPlan(plan, signer, vctx) {
|
|
19698
|
+
const approvalHashes = [];
|
|
19699
|
+
const residual = () => approvalHashes.length === 0 ? "" : ` An approval already landed (tx ${approvalHashes[approvalHashes.length - 1]}): an allowance of exactly ${plan.amountIn} to ${plan.spender} stands. It authorizes only that amount; the next exchange uses or replaces it.`;
|
|
19700
|
+
const sendLeg = async (leg) => {
|
|
19701
|
+
const request = { to: leg.tx.to, data: leg.tx.data, value: 0n, chainId: leg.tx.chainId };
|
|
19702
|
+
validateUnsignedTx({ ...request }, leg.expected, vctx);
|
|
19703
|
+
const sent = await signer.sendTransaction(request);
|
|
19704
|
+
const receipt = await sent.wait();
|
|
19705
|
+
if (!receipt)
|
|
19706
|
+
throw new Error(`PSM ${leg.step} tx ${sent.hash} returned no receipt`);
|
|
19707
|
+
if (receipt.status !== 1)
|
|
19708
|
+
throw new Error(`PSM ${leg.step} tx ${sent.hash} reverted`);
|
|
19709
|
+
return { hash: sent.hash, blockNumber: receipt.blockNumber };
|
|
19710
|
+
};
|
|
19711
|
+
for (const leg of plan.approvals) {
|
|
19712
|
+
const landed = await sendLeg(leg).catch((e) => {
|
|
19713
|
+
throw new Error(`PSM ${leg.step} failed: ${e instanceof Error ? e.message : String(e)}.${residual()}`);
|
|
19714
|
+
});
|
|
19715
|
+
approvalHashes.push(landed.hash);
|
|
19716
|
+
}
|
|
19717
|
+
const exec = await sendLeg(plan.exec).catch((e) => {
|
|
19718
|
+
throw new Error(`PSM ${plan.exec.step} failed: ${e instanceof Error ? e.message : String(e)}.${residual()}`);
|
|
19719
|
+
});
|
|
19720
|
+
return { ...exec, approvalHashes };
|
|
19721
|
+
}
|
|
19722
|
+
|
|
19723
|
+
// src/utils/lit-action-chain-name.ts
|
|
19724
|
+
var LIT_ACTION_CHAIN_NAMES = Object.freeze({
|
|
19725
|
+
1: "ethereum",
|
|
19726
|
+
11155111: "sepolia",
|
|
19727
|
+
1337: "hardhat",
|
|
19728
|
+
31337: "hardhat"
|
|
19729
|
+
});
|
|
19730
|
+
function litActionChainNameForChainId(chainId) {
|
|
19731
|
+
const name = LIT_ACTION_CHAIN_NAMES[Number(chainId)];
|
|
19732
|
+
if (!name) {
|
|
19733
|
+
throw new Error(
|
|
19734
|
+
`Unsupported chainId ${String(chainId)} for a Lit Action (supported: ${Object.keys(LIT_ACTION_CHAIN_NAMES).join(", ")})`
|
|
19735
|
+
);
|
|
19736
|
+
}
|
|
19737
|
+
return name;
|
|
19738
|
+
}
|
|
18899
19739
|
|
|
18900
19740
|
// src/modules/mock/mock-token-manager.module.ts
|
|
18901
19741
|
var MockTokenManager = class {
|
|
@@ -19880,6 +20720,18 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
19880
20720
|
*/
|
|
19881
20721
|
static async create(config) {
|
|
19882
20722
|
const userProvidedServiceEndpoint = config.serviceEndpoint;
|
|
20723
|
+
if (typeof config.chainId === "number" && config.provider) {
|
|
20724
|
+
const mismatch = await describeProviderChainMismatch(config.provider, config.chainId);
|
|
20725
|
+
if (mismatch) {
|
|
20726
|
+
return failure(
|
|
20727
|
+
new SDKError({
|
|
20728
|
+
message: mismatch,
|
|
20729
|
+
category: "CONFIGURATION" /* CONFIGURATION */,
|
|
20730
|
+
severity: "HIGH" /* HIGH */
|
|
20731
|
+
})
|
|
20732
|
+
);
|
|
20733
|
+
}
|
|
20734
|
+
}
|
|
19883
20735
|
const enrichedConfig = await _DiamondHandsSDK.enrichConfigWithNetworkDefaults(config);
|
|
19884
20736
|
if (userProvidedServiceEndpoint) {
|
|
19885
20737
|
enrichedConfig.serviceEndpoint = userProvidedServiceEndpoint;
|
|
@@ -20520,6 +21372,53 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
20520
21372
|
if (this.config.debug) {
|
|
20521
21373
|
log.info(` Position PKP ID: ${position2.pkpId}`);
|
|
20522
21374
|
}
|
|
21375
|
+
const termsResult = await this.getTermsWithFees();
|
|
21376
|
+
if (!termsResult.success) {
|
|
21377
|
+
return {
|
|
21378
|
+
success: false,
|
|
21379
|
+
error: `Cannot verify the loan cap before minting: ${termsResult.error.message}`
|
|
21380
|
+
};
|
|
21381
|
+
}
|
|
21382
|
+
const positionTermMonths = Number(position2.selectedTerm);
|
|
21383
|
+
const positionTerm = termsResult.value.terms.find(
|
|
21384
|
+
(term) => term.termMonths === positionTermMonths
|
|
21385
|
+
);
|
|
21386
|
+
if (!positionTerm) {
|
|
21387
|
+
return {
|
|
21388
|
+
success: false,
|
|
21389
|
+
error: `Cannot verify the loan cap before minting: TermManager has no fee entry for the position's ${positionTermMonths}-month term`
|
|
21390
|
+
};
|
|
21391
|
+
}
|
|
21392
|
+
const currentDebtWei = BigInt(position2.ucdDebt);
|
|
21393
|
+
const baseFeeWei = baseMintFeeWei(
|
|
21394
|
+
requestAmountWei,
|
|
21395
|
+
positionTerm.originationFeeBps
|
|
21396
|
+
);
|
|
21397
|
+
if (debtAfterMintExceedsLoanCap({
|
|
21398
|
+
currentDebtWei,
|
|
21399
|
+
mintAmountWei: requestAmountWei,
|
|
21400
|
+
mintFeeWei: baseFeeWei,
|
|
21401
|
+
maxLoanWei: maxLoanValueWei
|
|
21402
|
+
})) {
|
|
21403
|
+
const debtAfterWei = currentDebtWei + requestAmountWei + baseFeeWei;
|
|
21404
|
+
const maxPrincipalUcd = Number(
|
|
21405
|
+
maxPrincipalWithinLoanCap({
|
|
21406
|
+
maxLoanWei: maxLoanValueWei,
|
|
21407
|
+
currentDebtWei,
|
|
21408
|
+
originationFeeBps: positionTerm.originationFeeBps
|
|
21409
|
+
}) / BigInt(10 ** 18)
|
|
21410
|
+
);
|
|
21411
|
+
return {
|
|
21412
|
+
success: false,
|
|
21413
|
+
error: `Amount ${request.amount} UCD plus the ${positionTerm.originationFeeBps / 100}% origination fee (${formatEther(baseFeeWei)} UCD) would take this loan's debt to ${formatEther(debtAfterWei)} UCD, above the protocol maximum of ${formatEther(maxLoanValueWei)} UCD. The largest amount you can mint now is ${maxPrincipalUcd} UCD.`
|
|
21414
|
+
};
|
|
21415
|
+
}
|
|
21416
|
+
if (this.config.debug) {
|
|
21417
|
+
log.info(
|
|
21418
|
+
`\u2705 Fee-inclusive loan cap check passed: ${formatEther(currentDebtWei + requestAmountWei + baseFeeWei)} UCD <= ${formatEther(maxLoanValueWei)} UCD`,
|
|
21419
|
+
{}
|
|
21420
|
+
);
|
|
21421
|
+
}
|
|
20523
21422
|
let pkpPublicKey;
|
|
20524
21423
|
let pkpEthAddress;
|
|
20525
21424
|
const pkpCache = this.cacheManager.getCache("pkp-data", {
|
|
@@ -20609,7 +21508,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
20609
21508
|
` UCDController: ${contracts?.UCDController || "MISSING"}`
|
|
20610
21509
|
);
|
|
20611
21510
|
}
|
|
20612
|
-
const chain = this.config.chain || (
|
|
21511
|
+
const chain = this.config.chain || litActionChainNameForChainId(network.chainId);
|
|
20613
21512
|
const devBitcoinProviderUrl = request.customBitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url;
|
|
20614
21513
|
if (this.config.debug) {
|
|
20615
21514
|
log.info(` Network: ${chain} (chainId: ${network.chainId})`);
|
|
@@ -21209,6 +22108,25 @@ Error data: none`
|
|
|
21209
22108
|
}
|
|
21210
22109
|
const selector = simError.selector;
|
|
21211
22110
|
const errorName = simError.errorName ?? (selector ? `Unknown error ${selector}` : "unknown");
|
|
22111
|
+
const MINT_GUARD_CODES = {
|
|
22112
|
+
"1": "MINT_GUARD_LOAN_MAX_EXCEEDED \u2014 the position's total debt after this mint (existing debt + principal + fee) exceeds maximumLoanValueUcd",
|
|
22113
|
+
"2": "MINT_GUARD_CIRCUIT_BREAKER_EXCEEDED \u2014 exceeds CircuitBreaker maxSingleLoanValue",
|
|
22114
|
+
"3": "MINT_GUARD_PSM_DAILY_EXCEEDED \u2014 exceeds the PSM daily mint limit"
|
|
22115
|
+
};
|
|
22116
|
+
let loanCapDiagnostics = "";
|
|
22117
|
+
if (selector === "0xe6dd4d41") {
|
|
22118
|
+
const guardCode = BigInt(
|
|
22119
|
+
"0x" + String(simError.data).slice(10).padStart(64, "0")
|
|
22120
|
+
).toString();
|
|
22121
|
+
const guardMeaning = MINT_GUARD_CODES[guardCode] ?? `unknown MintGuardFailed code ${guardCode}`;
|
|
22122
|
+
loanCapDiagnostics = `
|
|
22123
|
+
|
|
22124
|
+
MintGuardFailed(${guardCode}): ${guardMeaning}
|
|
22125
|
+
Lit mintAmount: ${validationResponse.mintAmount}
|
|
22126
|
+
Lit mintFee: ${validationResponse.mintFee}
|
|
22127
|
+
Lit newDebt: ${validationResponse.newDebt}
|
|
22128
|
+
Protocol maximum: ${formatEther(maxLoanValueWei)} UCD (fee-inclusive)`;
|
|
22129
|
+
}
|
|
21212
22130
|
const mintDebtDiagnostics = selector === "0xb9d419a7" || // DebtUpdateVerificationFailed()
|
|
21213
22131
|
selector === "0xc7e20553" || // DebtUpdateVerificationFailedDetailed(...)
|
|
21214
22132
|
selector === "0x0cfd2a97" || // MintVerificationMismatch(uint256,uint256)
|
|
@@ -21254,7 +22172,7 @@ Quantum Timing Analysis:
|
|
|
21254
22172
|
Error selector: ${selector}
|
|
21255
22173
|
Error data: ${simError.data}
|
|
21256
22174
|
Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
|
|
21257
|
-
Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
22175
|
+
Message: ${causeMessage}${quantumContext}${loanCapDiagnostics}${mintDebtDiagnostics}`
|
|
21258
22176
|
);
|
|
21259
22177
|
}
|
|
21260
22178
|
const tx = await sendEip1559Transaction({
|
|
@@ -23851,7 +24769,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
23851
24769
|
` BTCSpendAuthorizer: ${contracts.BTCSpendAuthorizer || "MISSING"}`
|
|
23852
24770
|
);
|
|
23853
24771
|
}
|
|
23854
|
-
const chain = this.config.chain || (
|
|
24772
|
+
const chain = this.config.chain || litActionChainNameForChainId(network.chainId);
|
|
23855
24773
|
const devBitcoinProviderUrl = this.config.bitcoinProviders?.[0]?.url;
|
|
23856
24774
|
if (this.config.debug) {
|
|
23857
24775
|
log.info(` Network: ${chain} (chainId: ${network.chainId})`);
|
|
@@ -24406,13 +25324,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
24406
25324
|
return { success: false, error: "Failed to resolve PKP public key" };
|
|
24407
25325
|
}
|
|
24408
25326
|
const chipotlePkpAddress = computeAddress(publicKey);
|
|
24409
|
-
const
|
|
24410
|
-
1: "ethereum",
|
|
24411
|
-
11155111: "sepolia",
|
|
24412
|
-
1337: "hardhat",
|
|
24413
|
-
31337: "hardhat"
|
|
24414
|
-
};
|
|
24415
|
-
const chainName = STANDALONE_CHAIN_NAMES[chainId];
|
|
25327
|
+
const chainName = LIT_ACTION_CHAIN_NAMES[chainId];
|
|
24416
25328
|
if (!chainName) {
|
|
24417
25329
|
return {
|
|
24418
25330
|
success: false,
|
|
@@ -25634,6 +26546,18 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
25634
26546
|
async getLoansByBorrower(borrower, pagination, orderBy, orderDirection) {
|
|
25635
26547
|
return this.loanQuery.getLoansByBorrower(borrower, pagination, orderBy, orderDirection);
|
|
25636
26548
|
}
|
|
26549
|
+
/**
|
|
26550
|
+
* Combined UCD debt (canonical wei string + display string) and by-status loan
|
|
26551
|
+
* counts for a borrower, from one pass over the subgraph. `source: "subgraph"` —
|
|
26552
|
+
* an INDEXED, informational figure that lags writes; never size a transaction
|
|
26553
|
+
* from it (repayments read the chain via `getPositionDetailsView`).
|
|
26554
|
+
*
|
|
26555
|
+
* Fails loud with a SUBGRAPH-category `SDKError` (cause attached) on any page
|
|
26556
|
+
* failure or past 10,000 positions; never returns a partial total.
|
|
26557
|
+
*/
|
|
26558
|
+
async getBorrowerUcdDebtSummary(borrower) {
|
|
26559
|
+
return this.loanQuery.getBorrowerUcdDebtSummary(borrower);
|
|
26560
|
+
}
|
|
25637
26561
|
/**
|
|
25638
26562
|
* Get all active loans
|
|
25639
26563
|
*
|
|
@@ -25759,11 +26683,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
25759
26683
|
}
|
|
25760
26684
|
/**
|
|
25761
26685
|
* Execute a PSM stablecoin → UCD swap.
|
|
25762
|
-
*
|
|
26686
|
+
* Approves EXACTLY `amountWei` to the PSM when the current allowance is short (resetting a
|
|
26687
|
+
* partial allowance to zero first); every leg is validated by the sign-guard before it is sent.
|
|
25763
26688
|
*
|
|
25764
26689
|
* @param params.stablecoinAddress - ERC-20 address of the stablecoin to swap in
|
|
25765
26690
|
* @param params.amountWei - Stablecoin amount in native decimals (bigint)
|
|
25766
|
-
* @param params.minUcdOutWei - Minimum UCD to receive;
|
|
26691
|
+
* @param params.minUcdOutWei - Minimum UCD to receive; must be > 0 (reverts on-chain if below)
|
|
25767
26692
|
* @param params.signer - Connected signer for the approval and swap transactions
|
|
25768
26693
|
*/
|
|
25769
26694
|
async psmSwap(params) {
|
|
@@ -25773,32 +26698,23 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
25773
26698
|
"SimplePSMV2 address not configured \u2014 provide contractAddresses.simplePsmV2"
|
|
25774
26699
|
);
|
|
25775
26700
|
}
|
|
25776
|
-
|
|
25777
|
-
|
|
25778
|
-
|
|
25779
|
-
|
|
25780
|
-
|
|
25781
|
-
|
|
25782
|
-
|
|
25783
|
-
if (allowance < params.amountWei) {
|
|
25784
|
-
const approveTx = await stablecoin.approve(psmAddress, MaxUint256);
|
|
25785
|
-
await approveTx.wait();
|
|
25786
|
-
}
|
|
25787
|
-
const psm = SimplePSMV2__factory.connect(psmAddress, params.signer);
|
|
25788
|
-
const tx = await psm.swap(params.stablecoinAddress, params.amountWei, params.minUcdOutWei);
|
|
25789
|
-
const receipt = await tx.wait();
|
|
25790
|
-
if (!receipt)
|
|
25791
|
-
throw new Error("PSM swap transaction receipt unavailable");
|
|
25792
|
-
return { hash: tx.hash, blockNumber: receipt.blockNumber };
|
|
26701
|
+
return this.runPsmExchange({
|
|
26702
|
+
direction: "swap",
|
|
26703
|
+
signer: params.signer,
|
|
26704
|
+
addresses: { psm: psmAddress, stablecoin: params.stablecoinAddress },
|
|
26705
|
+
amountIn: params.amountWei,
|
|
26706
|
+
minOut: params.minUcdOutWei
|
|
26707
|
+
});
|
|
25793
26708
|
}
|
|
25794
26709
|
/**
|
|
25795
26710
|
* Execute a PSM UCD → stablecoin redeem.
|
|
25796
|
-
*
|
|
25797
|
-
* UCDToken.burn(from, amount) calls
|
|
26711
|
+
* Approves EXACTLY `ucdAmountWei` of UCD to the UCDController (not the PSM) to satisfy the
|
|
26712
|
+
* M-4 burn allowance guard: UCDToken.burn(from, amount) calls
|
|
26713
|
+
* _spendAllowance(from, msg.sender=ucdController, amount).
|
|
25798
26714
|
*
|
|
25799
26715
|
* @param params.stablecoinAddress - ERC-20 address of the stablecoin to receive
|
|
25800
26716
|
* @param params.ucdAmountWei - UCD amount to redeem (18 decimals, bigint)
|
|
25801
|
-
* @param params.minStablecoinOutWei - Minimum stablecoin to receive
|
|
26717
|
+
* @param params.minStablecoinOutWei - Minimum stablecoin to receive; must be > 0
|
|
25802
26718
|
* @param params.signer - Connected signer
|
|
25803
26719
|
*/
|
|
25804
26720
|
async psmRedeem(params) {
|
|
@@ -25811,23 +26727,51 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
25811
26727
|
throw new Error("UCDToken address not configured \u2014 provide contractAddresses.ucdToken");
|
|
25812
26728
|
if (!ucdControllerAddress)
|
|
25813
26729
|
throw new Error("UCDController address not configured \u2014 provide contractAddresses.ucdController");
|
|
25814
|
-
|
|
25815
|
-
|
|
25816
|
-
|
|
25817
|
-
|
|
25818
|
-
|
|
25819
|
-
|
|
25820
|
-
|
|
25821
|
-
|
|
25822
|
-
|
|
25823
|
-
|
|
25824
|
-
|
|
25825
|
-
|
|
25826
|
-
|
|
25827
|
-
|
|
25828
|
-
|
|
25829
|
-
|
|
25830
|
-
|
|
26730
|
+
return this.runPsmExchange({
|
|
26731
|
+
direction: "redeem",
|
|
26732
|
+
signer: params.signer,
|
|
26733
|
+
addresses: {
|
|
26734
|
+
psm: psmAddress,
|
|
26735
|
+
stablecoin: params.stablecoinAddress,
|
|
26736
|
+
ucdToken: ucdAddress,
|
|
26737
|
+
ucdController: ucdControllerAddress
|
|
26738
|
+
},
|
|
26739
|
+
amountIn: params.ucdAmountWei,
|
|
26740
|
+
minOut: params.minStablecoinOutWei
|
|
26741
|
+
});
|
|
26742
|
+
}
|
|
26743
|
+
/**
|
|
26744
|
+
* Shared PSM path: plan + validate every leg against the chain the SIGNER is on, then send.
|
|
26745
|
+
* The signer's chain must match the SDK's configured chain — the addresses came from it.
|
|
26746
|
+
*/
|
|
26747
|
+
async runPsmExchange(params) {
|
|
26748
|
+
const provider = params.signer.provider;
|
|
26749
|
+
if (!provider) {
|
|
26750
|
+
throw new Error(`PSM ${params.direction} needs a signer connected to a provider`);
|
|
26751
|
+
}
|
|
26752
|
+
const configuredChainId = this.config.chainId ?? this.config.networkOverride?.chainId;
|
|
26753
|
+
if (typeof configuredChainId !== "number") {
|
|
26754
|
+
throw new Error(`PSM ${params.direction} needs the SDK's chainId \u2014 configure chainId`);
|
|
26755
|
+
}
|
|
26756
|
+
const mismatch = await describeProviderChainMismatch(provider, configuredChainId);
|
|
26757
|
+
if (mismatch)
|
|
26758
|
+
throw new Error(mismatch);
|
|
26759
|
+
const vctx = {
|
|
26760
|
+
chainId: configuredChainId,
|
|
26761
|
+
network: String(configuredChainId),
|
|
26762
|
+
contracts: {}
|
|
26763
|
+
};
|
|
26764
|
+
const plan = await planPsmExchange({
|
|
26765
|
+
direction: params.direction,
|
|
26766
|
+
owner: await params.signer.getAddress(),
|
|
26767
|
+
addresses: params.addresses,
|
|
26768
|
+
amountIn: params.amountIn,
|
|
26769
|
+
minOut: params.minOut,
|
|
26770
|
+
vctx,
|
|
26771
|
+
reads: psmExchangeReadsFromProvider(provider, params.addresses.psm)
|
|
26772
|
+
});
|
|
26773
|
+
const { hash, blockNumber } = await executePsmPlan(plan, params.signer, vctx);
|
|
26774
|
+
return { hash, blockNumber };
|
|
25831
26775
|
}
|
|
25832
26776
|
/**
|
|
25833
26777
|
* Wait for the subgraph to index up to (and including) the given block number.
|
|
@@ -26005,18 +26949,30 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
26005
26949
|
const user = await signer.getAddress();
|
|
26006
26950
|
const registry = AgentDelegationRegistry__factory.connect(addr, signer);
|
|
26007
26951
|
let agentAddress;
|
|
26008
|
-
const
|
|
26009
|
-
|
|
26010
|
-
|
|
26952
|
+
const [isActive, record, latestBlock] = await Promise.all([
|
|
26953
|
+
registry.isAgentActive(user),
|
|
26954
|
+
registry.agentOf(user),
|
|
26955
|
+
this.getProviderOrThrow().getBlock("latest")
|
|
26956
|
+
]);
|
|
26957
|
+
if (!latestBlock) {
|
|
26958
|
+
throw new SDKError({
|
|
26959
|
+
message: "Could not read the latest block to check the agent registration's expiry",
|
|
26960
|
+
category: "NETWORK" /* NETWORK */,
|
|
26961
|
+
severity: "HIGH" /* HIGH */
|
|
26962
|
+
});
|
|
26963
|
+
}
|
|
26964
|
+
const plan = planAgentBinding({ isActive, record, nowSeconds: latestBlock.timestamp });
|
|
26965
|
+
if (plan.kind === "reuse") {
|
|
26966
|
+
agentAddress = plan.agent;
|
|
26011
26967
|
} else {
|
|
26012
26968
|
agentAddress = await mintAgentPkp({
|
|
26013
26969
|
serviceEndpoint: this.config.serviceEndpoint,
|
|
26014
26970
|
authHeader: this.serverSession ? () => this.serverSession.getAuthHeader() : void 0
|
|
26015
26971
|
});
|
|
26016
26972
|
const validitySeconds = options?.agentValiditySeconds ?? 90 * 24 * 60 * 60;
|
|
26017
|
-
const validUntil =
|
|
26018
|
-
const
|
|
26019
|
-
await
|
|
26973
|
+
const validUntil = latestBlock.timestamp + validitySeconds;
|
|
26974
|
+
const bindTx = plan.kind === "rotate" ? await registry.rotateAgent(user, agentAddress, validUntil) : await registry.registerAgent(user, agentAddress, validUntil);
|
|
26975
|
+
await bindTx.wait();
|
|
26020
26976
|
}
|
|
26021
26977
|
const currentDelegate = await getPositionDelegate(pid, signer, pdrAddr).catch(
|
|
26022
26978
|
() => null
|