@gvnrdao/dh-sdk 0.0.324 → 0.0.327

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.mjs CHANGED
@@ -360,7 +360,7 @@ var init_deployment_addresses = __esm({
360
360
  CARRY_AGENT_REGISTRY: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7",
361
361
  CARRY_AGENT_REGISTRY_IMPL: "0x1b11a3D5e46F3672E643c997f0d569DFf0b64a01",
362
362
  AGENT_DELEGATION_REGISTRY: "0x6AE7fc6bE0925126b9D25b1B7Ce5AD5d127ec025",
363
- AGENT_DELEGATION_REGISTRY_IMPL: "0x80C47350274513688e348D02506477A96B4afCCb",
363
+ AGENT_DELEGATION_REGISTRY_IMPL: "0xE931E4b9ceF2A48Ac7D57192da4B523756cFC310",
364
364
  DH_AGENT_DELEGATE: "0xa5A14489ae883960df6E1463A41a47aBE9Ecc5AF"
365
365
  }
366
366
  };
@@ -630,7 +630,7 @@ var init_deployment_addresses = __esm({
630
630
  LOCALHOST_DEPLOYMENT = {
631
631
  network: "localhost",
632
632
  chainId: 1337,
633
- timestamp: "2026-07-23T17:13:42.777Z",
633
+ timestamp: "2026-07-20T19:45:58.514Z",
634
634
  deployer: "",
635
635
  contracts: {
636
636
  MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
@@ -650,30 +650,6 @@ var init_deployment_addresses = __esm({
650
650
  PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
651
651
  OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
652
652
  PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
653
- },
654
- latestEnv: {
655
- UCD_TOKEN: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
656
- UCD_CONTROLLER: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
657
- POSITION_MANAGER: "0x851356ae760d987E095750cCeb3bC6014560891C",
658
- PKP_VALIDATION_REGISTRY: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A",
659
- PKP_VALIDATION_CID_V1: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
660
- PKP_VALIDATION_CID_V3: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
661
- PKP_ETH_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
662
- OPERATION_AUTHORIZATION_REGISTRY: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
663
- UCD_MINT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
664
- UCD_MINT_VALIDATOR_VERSION: 1,
665
- BTC_WITHDRAWAL_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
666
- BTC_WITHDRAWAL_VALIDATOR_VERSION: 3,
667
- UPDATE_BALANCE_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
668
- UPDATE_BALANCE_VALIDATOR_VERSION: 1,
669
- PROCESS_PAYMENT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
670
- PROCESS_PAYMENT_VALIDATOR_VERSION: 1,
671
- EXTEND_POSITION_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
672
- EXTEND_POSITION_VALIDATOR_VERSION: 1,
673
- POSITION_MANAGER_CORE_MODULE: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
674
- LOAN_OPERATIONS_MANAGER_MODULE: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
675
- TERM_MANAGER_MODULE: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
676
- COMMUNITY_MANAGER_MODULE: ""
677
653
  }
678
654
  };
679
655
  LOCALHOST_CONTRACTS = {
@@ -5528,8 +5504,14 @@ function getMainnetConfig() {
5528
5504
  agentModuleFactory: MAINNET_CONTRACTS.AgentModuleFactory || ""
5529
5505
  },
5530
5506
  subgraphs: {
5507
+ // KNOWN-WRONG placeholder (see GOAL-PLAN step 2 / dogfood F-CLI-4): this is a
5508
+ // SEPOLIA subgraph id in a mainnet (chainId 1) config, and this gateway host
5509
+ // needs an Authorization key the SDK does not (and must not) hold — so the
5510
+ // URL is doubly unusable. The `CCTPsd…` path segment is the subgraph's
5511
+ // PUBLIC ID, not a credential. The fix is a chain-correct SERVER-side proxy
5512
+ // (see mcp/docs/SUBGRAPH-PROXY-PLAN.md) — same rule as rpcUrls above:
5513
+ // never ship an API-keyed URL in a client config.
5531
5514
  diamondHandsUrl: "https://gateway-arbitrum.network.thegraph.com/api/subgraphs/id/CCTPsdYqco2jChDLLBQTbdJWwoukVoMt1cXeR9ti6r9A"
5532
- // Published Sepolia subgraph on Arbitrum One
5533
5515
  },
5534
5516
  litNetwork: "chipotle",
5535
5517
  debug: false
@@ -6326,7 +6308,7 @@ async function generateMintAuthorization(positionId, amount, chainId, signerOrPr
6326
6308
  signature
6327
6309
  };
6328
6310
  }
6329
- async function generatePaymentAuthorization(positionId, amount, chainId, signer) {
6311
+ async function generatePaymentAuthorization(positionId, amount, chainId, signerOrPrecomputed) {
6330
6312
  if (typeof positionId !== "string") {
6331
6313
  throw new Error(`positionId must be a string, got ${typeof positionId}`);
6332
6314
  }
@@ -6336,6 +6318,18 @@ async function generatePaymentAuthorization(positionId, amount, chainId, signer)
6336
6318
  if (typeof chainId !== "number") {
6337
6319
  throw new Error(`chainId must be a number, got ${typeof chainId}`);
6338
6320
  }
6321
+ if (typeof signerOrPrecomputed === "object" && "signature" in signerOrPrecomputed && !("signMessage" in signerOrPrecomputed)) {
6322
+ const { timestamp: timestamp2, signature: signature2 } = signerOrPrecomputed;
6323
+ return {
6324
+ positionId,
6325
+ timestamp: timestamp2,
6326
+ chainId,
6327
+ amount: amount.toString(),
6328
+ action: "make-payment",
6329
+ signature: signature2
6330
+ };
6331
+ }
6332
+ const signer = signerOrPrecomputed;
6339
6333
  const timestamp = await calculateNextQuantumTimestamp();
6340
6334
  if (typeof timestamp !== "number") {
6341
6335
  throw new Error(`timestamp must be a number, got ${typeof timestamp}`);
@@ -6577,6 +6571,26 @@ async function generateExtendAuthorization(positionId, newTerm, chainId, signerO
6577
6571
  };
6578
6572
  }
6579
6573
 
6574
+ // src/utils/authorization-provider.utils.ts
6575
+ async function resolveAuthorizationInput(provider, signer, ctx) {
6576
+ if (!provider)
6577
+ return signer;
6578
+ const result = await provider(ctx);
6579
+ if (!result || typeof result.signature !== "string" || result.signature.length === 0) {
6580
+ throw new Error(
6581
+ "authorizationProvider returned no signature \u2014 refusing to fall back to the connected signer, which would prompt for the owner quorum this provider exists to avoid"
6582
+ );
6583
+ }
6584
+ if (!Number.isInteger(result.timestamp)) {
6585
+ throw new Error(
6586
+ `authorizationProvider returned a non-integer timestamp (${String(
6587
+ result.timestamp
6588
+ )}) \u2014 the protocol call must carry the same value the envelope was signed over`
6589
+ );
6590
+ }
6591
+ return { timestamp: result.timestamp, signature: result.signature };
6592
+ }
6593
+
6580
6594
  // src/utils/eip712-login.ts
6581
6595
  import { hexlify, randomBytes } from "ethers";
6582
6596
  function buildLoginDomain(chainId) {
@@ -20002,11 +20016,20 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
20002
20016
  {}
20003
20017
  );
20004
20018
  }
20019
+ const mintAuthInput = await resolveAuthorizationInput(
20020
+ request.authorizationProvider,
20021
+ signer2,
20022
+ {
20023
+ positionId: request.positionId,
20024
+ timestamp: calculateNextQuantumTimestamp(),
20025
+ amount: requestAmountWei.toString()
20026
+ }
20027
+ );
20005
20028
  const auth = await generateMintAuthorization(
20006
20029
  request.positionId,
20007
20030
  BigInt(requestAmountWei.toString()),
20008
20031
  chainId,
20009
- signer2
20032
+ mintAuthInput
20010
20033
  );
20011
20034
  if (this.config.debug) {
20012
20035
  log.info(` Authorization generated (attempt ${attempt})`, {});
@@ -21597,7 +21620,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21597
21620
  * @param _selectedTerm - Extension term in months
21598
21621
  * @returns Transaction receipt
21599
21622
  */
21600
- async extendPosition(_positionId, _selectedTerm, _rpcUrl, _bitcoinRpcUrl) {
21623
+ async extendPosition(_positionId, _selectedTerm, _rpcUrl, _bitcoinRpcUrl, _authorizationProvider) {
21601
21624
  this.ensureInitialized();
21602
21625
  if (typeof _positionId !== "string") {
21603
21626
  throw new Error(`positionId must be a string, got ${typeof _positionId}`);
@@ -21691,11 +21714,23 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21691
21714
  }
21692
21715
  }
21693
21716
  const signer = this.getSignerOrThrow();
21717
+ const extendAuthInput = await resolveAuthorizationInput(
21718
+ _authorizationProvider,
21719
+ signer,
21720
+ {
21721
+ positionId: _positionId,
21722
+ timestamp: calculateNextQuantumTimestamp(),
21723
+ // The extend envelope binds the TERM, not an amount. Carried as "0" so
21724
+ // the context shape stays uniform across the four operations.
21725
+ amount: "0",
21726
+ selectedTerm: _selectedTerm
21727
+ }
21728
+ );
21694
21729
  const auth = await generateExtendAuthorization(
21695
21730
  _positionId,
21696
21731
  _selectedTerm,
21697
21732
  chainId,
21698
- signer
21733
+ extendAuthInput
21699
21734
  );
21700
21735
  if (this.config.debug) {
21701
21736
  log.info(" Extend authorization generated", {
@@ -21951,7 +21986,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21951
21986
  positionId,
21952
21987
  selectedTerm,
21953
21988
  request.rpcUrl,
21954
- request.customBitcoinRpcUrl
21989
+ request.customBitcoinRpcUrl,
21990
+ request.authorizationProvider
21955
21991
  );
21956
21992
  let newExpiryAt;
21957
21993
  try {
@@ -22479,11 +22515,20 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22479
22515
  }
22480
22516
  let auth;
22481
22517
  try {
22518
+ const paymentAuthInput = await resolveAuthorizationInput(
22519
+ request.authorizationProvider,
22520
+ signer,
22521
+ {
22522
+ positionId: request.positionId,
22523
+ timestamp: calculateNextQuantumTimestamp(),
22524
+ amount: paymentAmountWei.toString()
22525
+ }
22526
+ );
22482
22527
  auth = await generatePaymentAuthorization(
22483
22528
  request.positionId,
22484
22529
  paymentAmountWei,
22485
22530
  chainId,
22486
- signer
22531
+ paymentAuthInput
22487
22532
  );
22488
22533
  } catch (authError) {
22489
22534
  if (authError.message && authError.message.includes("BigNumber")) {
@@ -22995,7 +23040,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22995
23040
  * @param customBitcoinRpcUrl - Optional custom Bitcoin RPC URL (for local testing)
22996
23041
  * @returns Withdrawal result with transaction details
22997
23042
  */
22998
- async withdrawBTC(positionId, withdrawalAddress, withdrawalAmount) {
23043
+ async withdrawBTC(positionId, withdrawalAddress, withdrawalAmount, options) {
22999
23044
  this.ensureInitialized();
23000
23045
  try {
23001
23046
  if (this.config.debug) {
@@ -23210,11 +23255,21 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
23210
23255
  {}
23211
23256
  );
23212
23257
  }
23258
+ const withdrawAuthInput = await resolveAuthorizationInput(
23259
+ options?.authorizationProvider,
23260
+ signer,
23261
+ {
23262
+ positionId,
23263
+ timestamp: calculateNextQuantumTimestamp(),
23264
+ amount: BigInt(withdrawalAmount).toString(),
23265
+ destinationAddress: withdrawalAddress
23266
+ }
23267
+ );
23213
23268
  const auth = await generateWithdrawAuthorization(
23214
23269
  positionId,
23215
23270
  BigInt(withdrawalAmount),
23216
23271
  chainId,
23217
- signer,
23272
+ withdrawAuthInput,
23218
23273
  withdrawalAddress
23219
23274
  // Pass destination address for message hash
23220
23275
  );
@@ -24288,6 +24343,11 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24288
24343
  * (`MAX_ATTESTATION_AGE = 1 hour`) and replay-protects via
24289
24344
  * `authorizedAt` binding.
24290
24345
  *
24346
+ * WHO PAYS: in **service mode** lit-ops-server performs BOTH steps and its
24347
+ * relayer funds the transaction — the borrower is never prompted, because the
24348
+ * Bitcoin withdrawal this cleans up after has already settled. In **direct
24349
+ * mode** (CLI / tests / hardhat) the SDK's own signer submits step 2.
24350
+ *
24291
24351
  * `invalidatorTxid` is either:
24292
24352
  * - the borrower's own authorized broadcast (classified "consumed-as-authorized"
24293
24353
  * by the LIT validator), once it has ≥6 confirmations on the BTC
@@ -24300,7 +24360,6 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24300
24360
  async recoverStaleSpend(params) {
24301
24361
  this.ensureInitialized();
24302
24362
  try {
24303
- const signer = this.getSignerOrThrow();
24304
24363
  const contractAddresses = this.getContractAddressesOrThrow();
24305
24364
  const btcSpendAuthorizerAddress = contractAddresses.btcSpendAuthorizer;
24306
24365
  if (!btcSpendAuthorizerAddress) {
@@ -24346,6 +24405,28 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24346
24405
  success: false,
24347
24406
  error: json.error ?? "stale-spend recovery request failed"
24348
24407
  };
24408
+ if (!attRes.success || !attRes.attestation) {
24409
+ return {
24410
+ success: false,
24411
+ failedStep: "attestation",
24412
+ error: attRes.error ?? attRes.reason ?? `LIT recovery attestation rejected${attRes.failedStep ? ` at step ${attRes.failedStep}` : ""}`
24413
+ };
24414
+ }
24415
+ if (!attRes.transactionHash && !attRes.alreadyCleared) {
24416
+ return {
24417
+ success: false,
24418
+ failedStep: "submit",
24419
+ error: "lit-ops-server returned a stale-spend attestation but did not submit it. The server is on a build without the recovery relayer, or its STALE_SPEND_RELAYER_PRIVATE_KEY is unset."
24420
+ };
24421
+ }
24422
+ return {
24423
+ success: true,
24424
+ ...attRes.transactionHash && {
24425
+ transactionHash: attRes.transactionHash
24426
+ },
24427
+ ...attRes.classification && { classification: attRes.classification },
24428
+ attestation: attRes.attestation
24429
+ };
24349
24430
  } else {
24350
24431
  const rpcUrl = params.rpcUrl ?? this.config.ethRpcUrl;
24351
24432
  if (!rpcUrl) {
@@ -24395,7 +24476,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24395
24476
  [
24396
24477
  "function cancelStaleSpendWithProof(bytes32 positionId, bytes32 utxoKey, uint256 authorizedAt, string calldata invalidatorTxid, uint256 attestationTimestamp, bytes calldata litSignature) external"
24397
24478
  ],
24398
- signer
24479
+ this.getSignerOrThrow()
24399
24480
  );
24400
24481
  const tx = await btcSpendAuthorizer["cancelStaleSpendWithProof"](
24401
24482
  attRes.attestation.positionId,
@@ -25203,10 +25284,22 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25203
25284
  "getLoanGrant",
25204
25285
  data
25205
25286
  );
25287
+ const raw = getBytes5(data);
25288
+ const withdrawScopeSupported = raw.length >= 128;
25289
+ const minWithdrawRatioBps = withdrawScopeSupported ? Number(
25290
+ AbiCoder2.defaultAbiCoder().decode(["uint32"], raw.slice(96, 128))[0]
25291
+ ) : 0;
25206
25292
  return {
25207
25293
  borrower,
25208
25294
  scopeBits: Number(scopeBits),
25209
- minCollateralRatioBps: Number(minCollateralRatioBps)
25295
+ minCollateralRatioBps: Number(minCollateralRatioBps),
25296
+ minWithdrawRatioBps,
25297
+ /**
25298
+ * True when the deployed registry is v2.1.0+ and therefore honours `SCOPE_WITHDRAW`. The
25299
+ * frontend gates its Withdrawal control on this so the control is absent — not disabled,
25300
+ * not "coming soon" — on a chain whose registry cannot support it.
25301
+ */
25302
+ withdrawScopeSupported
25210
25303
  };
25211
25304
  }
25212
25305
  /**
@@ -25248,10 +25341,14 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25248
25341
  const mintEnabled = hasGrant && (scopes & 4) !== 0;
25249
25342
  const floor = grant.minCollateralRatioBps;
25250
25343
  const agent = user ? await registry.agentOf(user) : null;
25344
+ const withdrawEnabled = hasGrant && grant.withdrawScopeSupported && (scopes & 8) !== 0;
25251
25345
  return {
25252
25346
  renewEnabled: hasGrant && (scopes & 1) !== 0,
25253
25347
  repayEnabled: hasGrant && (scopes & 2) !== 0,
25254
25348
  mintEnabled,
25349
+ withdrawEnabled,
25350
+ withdrawScopeSupported: grant.withdrawScopeSupported,
25351
+ minWithdrawRatioBps: withdrawEnabled ? grant.minWithdrawRatioBps : 0,
25255
25352
  minCollateralRatioBps: mintEnabled ? floor : 0,
25256
25353
  effectiveMintFloorBps: mintEnabled ? Math.max(floor, protocolMin) : 0,
25257
25354
  protocolMinFloorBps: protocolMin,
@@ -25437,8 +25534,83 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25437
25534
  const receipt = await tx.wait();
25438
25535
  return { hash: tx.hash, blockNumber: receipt?.blockNumber ?? 0 };
25439
25536
  }
25440
- /** Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on. */
25441
- async assertMintFloorAtOrAboveProtocolMin(floorBps) {
25537
+ /**
25538
+ * Grant the agent the WITHDRAW scope for a position (registry v2.1.0+). Borrower-signed.
25539
+ *
25540
+ * CONTRACT WALLETS ONLY, and this is enforced here rather than on-chain. The withdrawal
25541
+ * validator's only non-borrower arm is `checkSafeAgentWithdrawAuthorization`, which requires a
25542
+ * factory-provenanced `AgentModule` whose `safe()` is the borrower — something an EOA cannot
25543
+ * have. The registry deliberately does NOT check wallet type ("Callable by any borrower (this
25544
+ * function does not itself require a Safe)"), so an EOA can record a grant that no code path
25545
+ * will ever satisfy. Refusing here keeps the supported path from writing a dead record.
25546
+ *
25547
+ * That refusal is ergonomics, NOT enforcement — `enableAutoWithdraw` is `external` and reachable
25548
+ * from any wallet. The real reason delegated withdrawal is closed to EOAs is that they can sign
25549
+ * inside the ~44s window themselves, so delegating buys them nothing and only adds risk.
25550
+ *
25551
+ * @param minWithdrawRatioBps Post-withdrawal collateral-ratio floor for AGENT withdrawals.
25552
+ * Clamped by the same governance minimum as the mint floor. **Inert at zero debt** — it
25553
+ * is a ratio, and the post-withdrawal ratio is infinite for any amount once debt is 0.
25554
+ * The operative bound there is the quorum-gated destination allowlist. Do not surface
25555
+ * this to users as a cap on how much an agent may withdraw.
25556
+ * There is no in-place raise: changing it means disable-then-re-enable, deliberately —
25557
+ * on the one scope that moves custodied BTC, incident response can only disable outright.
25558
+ */
25559
+ async enableAutoWithdraw(positionId, minWithdrawRatioBps, options) {
25560
+ const pid = this.toBytes32(positionId);
25561
+ await this.assertBorrowerIsContractWallet(pid);
25562
+ await this.assertMintFloorAtOrAboveProtocolMin(minWithdrawRatioBps, "withdraw");
25563
+ const { registry, agentAddress } = await this.prepareAgentDelegation(pid, options);
25564
+ const tx = await registry.enableAutoWithdraw(pid, minWithdrawRatioBps);
25565
+ const receipt = await tx.wait();
25566
+ return { hash: tx.hash, blockNumber: receipt?.blockNumber ?? 0, agentAddress };
25567
+ }
25568
+ /**
25569
+ * Revoke the WITHDRAW scope for a position (also zeroes its withdraw floor). Borrower-signed.
25570
+ *
25571
+ * No wallet-type check: revoking is the safe direction, and an EOA that recorded a grant before
25572
+ * this guard existed must be able to clear it.
25573
+ */
25574
+ async disableAutoWithdraw(positionId) {
25575
+ const addr = this.agentDelegationRegistryOrThrow();
25576
+ const signer = this.getSignerOrThrow();
25577
+ const registry = AgentDelegationRegistry__factory.connect(addr, signer);
25578
+ const pid = this.toBytes32(positionId);
25579
+ const tx = await registry.disableAutoWithdraw(pid);
25580
+ const receipt = await tx.wait();
25581
+ await this.clearGate1IfNoScopesRemain(pid);
25582
+ return { hash: tx.hash, blockNumber: receipt?.blockNumber ?? 0 };
25583
+ }
25584
+ /**
25585
+ * Layer 2 of the withdrawal-delegation gating: refuse to write a grant that could never be
25586
+ * satisfied. See `enableAutoWithdraw` for why this lives here and not in the registry.
25587
+ */
25588
+ async assertBorrowerIsContractWallet(pid) {
25589
+ const details = await this.getPositionDetailsView(pid);
25590
+ const borrower = details?.borrower;
25591
+ if (!borrower) {
25592
+ throw new SDKError({
25593
+ message: `Could not resolve the borrower for position ${pid} \u2014 refusing to write a delegation grant against an unverified position`,
25594
+ category: "VALIDATION" /* VALIDATION */,
25595
+ severity: "HIGH" /* HIGH */
25596
+ });
25597
+ }
25598
+ const code = await this.getProviderOrThrow().getCode(borrower);
25599
+ if (!code || code === "0x") {
25600
+ throw new SDKError({
25601
+ message: `Withdrawal delegation is only available to contract wallets. Borrower ${borrower} is an EOA, and the withdrawal validator has no delegate arm for one \u2014 an EOA can sign inside the withdrawal quantum window itself, so the grant would be recorded on-chain and never honoured.`,
25602
+ category: "VALIDATION" /* VALIDATION */,
25603
+ severity: "MEDIUM" /* MEDIUM */
25604
+ });
25605
+ }
25606
+ }
25607
+ /**
25608
+ * Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on.
25609
+ *
25610
+ * `agentMintFloorMinBps` is the governance floor-of-floors for BOTH the mint and withdraw
25611
+ * grants — one lever, one invariant — so `scope` only shapes the message.
25612
+ */
25613
+ async assertMintFloorAtOrAboveProtocolMin(floorBps, scope = "mint") {
25442
25614
  if (!Number.isInteger(floorBps) || floorBps <= 0) {
25443
25615
  throw new SDKError({
25444
25616
  message: `Invalid collateral floor: ${floorBps} bps \u2014 must be a positive integer`,
@@ -25449,7 +25621,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25449
25621
  const protocolMin = await this.getAgentMintFloorMinBps();
25450
25622
  if (protocolMin === 0) {
25451
25623
  throw new SDKError({
25452
- message: "Agent minting is not configured on this network (agentMintFloorMinBps is 0) \u2014 the registry would reject every mint grant",
25624
+ message: `Agent ${scope === "mint" ? "minting" : "withdrawal"} is not configured on this network (agentMintFloorMinBps is 0) \u2014 the registry would reject every ${scope} grant`,
25453
25625
  category: "CONFIGURATION" /* CONFIGURATION */,
25454
25626
  severity: "HIGH" /* HIGH */
25455
25627
  });
@@ -2,6 +2,7 @@
2
2
  * Loan Operations Interfaces
3
3
  */
4
4
  import { LoanStatus } from "../../types/loanStatus";
5
+ import type { AuthorizationProvider } from "../../utils/authorization-provider.utils";
5
6
  export type BitcoinAddresses = {
6
7
  mainnet: string;
7
8
  testnet: string;
@@ -176,6 +177,18 @@ export interface UCDMintRequest {
176
177
  stubbedAvailableBtcSats?: string;
177
178
  stubbedAuthorizedSpendsHash?: string;
178
179
  };
180
+ /**
181
+ * Supplies the borrower authorization envelope instead of the connected
182
+ * signer — the seam a delegated Safe uses so its agent signs instantly
183
+ * rather than the SDK asking for an owner quorum inside a quantum window.
184
+ *
185
+ * Omit it and nothing changes: the SDK signs with the connected signer
186
+ * exactly as before, which is what every EOA borrower does.
187
+ *
188
+ * See `utils/authorization-provider.utils.ts` for why this is a callback and
189
+ * not a `{ timestamp, signature }` value.
190
+ */
191
+ authorizationProvider?: AuthorizationProvider;
179
192
  }
180
193
  /**
181
194
  * UCD Mint Result Interface
@@ -213,6 +226,18 @@ export interface RenewPositionResult {
213
226
  export interface RenewPositionRequest {
214
227
  positionId: string;
215
228
  selectedTerm: number;
229
+ /**
230
+ * Supplies the borrower authorization envelope instead of the connected
231
+ * signer — the seam a delegated Safe uses so its agent signs instantly
232
+ * rather than the SDK asking for an owner quorum inside a quantum window.
233
+ *
234
+ * Omit it and nothing changes: the SDK signs with the connected signer
235
+ * exactly as before, which is what every EOA borrower does.
236
+ *
237
+ * See `utils/authorization-provider.utils.ts` for why this is a callback and
238
+ * not a `{ timestamp, signature }` value.
239
+ */
240
+ authorizationProvider?: AuthorizationProvider;
216
241
  }
217
242
  /**
218
243
  * BTC Withdrawal Result Interface
@@ -282,6 +307,18 @@ export interface PartialPaymentRequest {
282
307
  positionId: string;
283
308
  paymentAmount: number;
284
309
  rpcUrl?: string;
310
+ /**
311
+ * Supplies the borrower authorization envelope instead of the connected
312
+ * signer — the seam a delegated Safe uses so its agent signs instantly
313
+ * rather than the SDK asking for an owner quorum inside a quantum window.
314
+ *
315
+ * Omit it and nothing changes: the SDK signs with the connected signer
316
+ * exactly as before, which is what every EOA borrower does.
317
+ *
318
+ * See `utils/authorization-provider.utils.ts` for why this is a callback and
319
+ * not a `{ timestamp, signature }` value.
320
+ */
321
+ authorizationProvider?: AuthorizationProvider;
285
322
  }
286
323
  /**
287
324
  * Confirm Balance Result Interface
@@ -18,6 +18,7 @@ import { SDKError } from "../utils/error-handler";
18
18
  import type { CreateLoanRequest, CreateLoanResult, LoanDataDetail, UCDMintRequest, UCDMintResult, PartialPaymentRequest, PartialPaymentResult, BTCWithdrawalResult, RenewPositionRequest, RenewPositionResult, LiquidationRequest, LiquidationResult, ConfirmBalanceRequest, ConfirmBalanceResult, TermsWithFeesResult } from "../interfaces/chunks/loan-operations.i";
19
19
  import type { DiamondHandsSDKConfig } from "../interfaces/chunks/config.i";
20
20
  import type { PKPData } from "../interfaces/chunks/pkp-integration.i";
21
+ import { type AuthorizationProvider } from "../utils/authorization-provider.utils";
21
22
  import type { DhServerLoginPayload } from "../utils/eip712-login";
22
23
  import { type ReconciledWithdrawal } from "../utils/withdrawal-reconciliation.utils";
23
24
  import { ContractManager } from "./contract/contract-manager.module";
@@ -581,7 +582,15 @@ export declare class DiamondHandsSDK {
581
582
  * @param customBitcoinRpcUrl - Optional custom Bitcoin RPC URL (for local testing)
582
583
  * @returns Withdrawal result with transaction details
583
584
  */
584
- withdrawBTC(positionId: string, withdrawalAddress: string, withdrawalAmount: number): Promise<BTCWithdrawalResult>;
585
+ withdrawBTC(positionId: string, withdrawalAddress: string, withdrawalAmount: number,
586
+ /**
587
+ * An options bag rather than a fifth positional argument: this method is
588
+ * already three positional strings/numbers deep, and the next caller to
589
+ * add one would have to guess the order.
590
+ */
591
+ options?: {
592
+ authorizationProvider?: AuthorizationProvider;
593
+ }): Promise<BTCWithdrawalResult>;
585
594
  /**
586
595
  * TEMPORARY (multi-UTXO withdrawal fix, Phase A): fetch the vault's confirmed
587
596
  * UTXO set from lit-ops-server (`GET /api/lit/vault-utxos`) so the caller can
@@ -739,6 +748,11 @@ export declare class DiamondHandsSDK {
739
748
  * (`MAX_ATTESTATION_AGE = 1 hour`) and replay-protects via
740
749
  * `authorizedAt` binding.
741
750
  *
751
+ * WHO PAYS: in **service mode** lit-ops-server performs BOTH steps and its
752
+ * relayer funds the transaction — the borrower is never prompted, because the
753
+ * Bitcoin withdrawal this cleans up after has already settled. In **direct
754
+ * mode** (CLI / tests / hardhat) the SDK's own signer submits step 2.
755
+ *
742
756
  * `invalidatorTxid` is either:
743
757
  * - the borrower's own authorized broadcast (classified "consumed-as-authorized"
744
758
  * by the LIT validator), once it has ≥6 confirmations on the BTC
@@ -1071,6 +1085,16 @@ export declare class DiamondHandsSDK {
1071
1085
  agentActive: boolean | null;
1072
1086
  /** Agent expiry (unix seconds) — the ONLY time bound on every grant. Null without `user`. */
1073
1087
  agentValidUntil: number | null;
1088
+ /** SCOPE_WITHDRAW granted. Always false where the registry predates v2.1.0. */
1089
+ withdrawEnabled: boolean;
1090
+ /** Does the DEPLOYED registry honour SCOPE_WITHDRAW at all (v2.1.0+)? */
1091
+ withdrawScopeSupported: boolean;
1092
+ /**
1093
+ * Grant's post-withdrawal collateral floor in bps; 0 when WITHDRAW is not granted.
1094
+ * INERT AT ZERO DEBT — a ratio cannot bound a withdrawal once debt is 0. Never present this
1095
+ * as a cap on how much an agent may withdraw.
1096
+ */
1097
+ minWithdrawRatioBps: number;
1074
1098
  }>;
1075
1099
  /** Governance floor-of-floors for agent mints (bps). The UI's minimum for the floor editor. */
1076
1100
  getAgentMintFloorMinBps(): Promise<number>;
@@ -1161,7 +1185,56 @@ export declare class DiamondHandsSDK {
1161
1185
  hash: string;
1162
1186
  blockNumber: number;
1163
1187
  }>;
1164
- /** Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on. */
1188
+ /**
1189
+ * Grant the agent the WITHDRAW scope for a position (registry v2.1.0+). Borrower-signed.
1190
+ *
1191
+ * CONTRACT WALLETS ONLY, and this is enforced here rather than on-chain. The withdrawal
1192
+ * validator's only non-borrower arm is `checkSafeAgentWithdrawAuthorization`, which requires a
1193
+ * factory-provenanced `AgentModule` whose `safe()` is the borrower — something an EOA cannot
1194
+ * have. The registry deliberately does NOT check wallet type ("Callable by any borrower (this
1195
+ * function does not itself require a Safe)"), so an EOA can record a grant that no code path
1196
+ * will ever satisfy. Refusing here keeps the supported path from writing a dead record.
1197
+ *
1198
+ * That refusal is ergonomics, NOT enforcement — `enableAutoWithdraw` is `external` and reachable
1199
+ * from any wallet. The real reason delegated withdrawal is closed to EOAs is that they can sign
1200
+ * inside the ~44s window themselves, so delegating buys them nothing and only adds risk.
1201
+ *
1202
+ * @param minWithdrawRatioBps Post-withdrawal collateral-ratio floor for AGENT withdrawals.
1203
+ * Clamped by the same governance minimum as the mint floor. **Inert at zero debt** — it
1204
+ * is a ratio, and the post-withdrawal ratio is infinite for any amount once debt is 0.
1205
+ * The operative bound there is the quorum-gated destination allowlist. Do not surface
1206
+ * this to users as a cap on how much an agent may withdraw.
1207
+ * There is no in-place raise: changing it means disable-then-re-enable, deliberately —
1208
+ * on the one scope that moves custodied BTC, incident response can only disable outright.
1209
+ */
1210
+ enableAutoWithdraw(positionId: string, minWithdrawRatioBps: number, options?: {
1211
+ agentValiditySeconds?: number;
1212
+ }): Promise<{
1213
+ hash: string;
1214
+ blockNumber: number;
1215
+ agentAddress?: string;
1216
+ }>;
1217
+ /**
1218
+ * Revoke the WITHDRAW scope for a position (also zeroes its withdraw floor). Borrower-signed.
1219
+ *
1220
+ * No wallet-type check: revoking is the safe direction, and an EOA that recorded a grant before
1221
+ * this guard existed must be able to clear it.
1222
+ */
1223
+ disableAutoWithdraw(positionId: string): Promise<{
1224
+ hash: string;
1225
+ blockNumber: number;
1226
+ }>;
1227
+ /**
1228
+ * Layer 2 of the withdrawal-delegation gating: refuse to write a grant that could never be
1229
+ * satisfied. See `enableAutoWithdraw` for why this lives here and not in the registry.
1230
+ */
1231
+ private assertBorrowerIsContractWallet;
1232
+ /**
1233
+ * Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on.
1234
+ *
1235
+ * `agentMintFloorMinBps` is the governance floor-of-floors for BOTH the mint and withdraw
1236
+ * grants — one lever, one invariant — so `scope` only shapes the message.
1237
+ */
1165
1238
  private assertMintFloorAtOrAboveProtocolMin;
1166
1239
  /**
1167
1240
  * Get Bitcoin balance for an address
@@ -326,7 +326,7 @@ var SEPOLIA_DEPLOYMENT = {
326
326
  CARRY_AGENT_REGISTRY: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7",
327
327
  CARRY_AGENT_REGISTRY_IMPL: "0x1b11a3D5e46F3672E643c997f0d569DFf0b64a01",
328
328
  AGENT_DELEGATION_REGISTRY: "0x6AE7fc6bE0925126b9D25b1B7Ce5AD5d127ec025",
329
- AGENT_DELEGATION_REGISTRY_IMPL: "0x80C47350274513688e348D02506477A96B4afCCb",
329
+ AGENT_DELEGATION_REGISTRY_IMPL: "0xE931E4b9ceF2A48Ac7D57192da4B523756cFC310",
330
330
  DH_AGENT_DELEGATE: "0xa5A14489ae883960df6E1463A41a47aBE9Ecc5AF"
331
331
  }
332
332
  };
@@ -538,7 +538,7 @@ var MAINNET_DEPLOYMENT = {
538
538
  var LOCALHOST_DEPLOYMENT = {
539
539
  network: "localhost",
540
540
  chainId: 1337,
541
- timestamp: "2026-07-23T17:13:42.777Z",
541
+ timestamp: "2026-07-20T19:45:58.514Z",
542
542
  deployer: "",
543
543
  contracts: {
544
544
  MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
@@ -558,30 +558,6 @@ var LOCALHOST_DEPLOYMENT = {
558
558
  PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
559
559
  OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
560
560
  PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
561
- },
562
- latestEnv: {
563
- UCD_TOKEN: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
564
- UCD_CONTROLLER: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
565
- POSITION_MANAGER: "0x851356ae760d987E095750cCeb3bC6014560891C",
566
- PKP_VALIDATION_REGISTRY: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A",
567
- PKP_VALIDATION_CID_V1: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
568
- PKP_VALIDATION_CID_V3: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
569
- PKP_ETH_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
570
- OPERATION_AUTHORIZATION_REGISTRY: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
571
- UCD_MINT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
572
- UCD_MINT_VALIDATOR_VERSION: 1,
573
- BTC_WITHDRAWAL_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
574
- BTC_WITHDRAWAL_VALIDATOR_VERSION: 3,
575
- UPDATE_BALANCE_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
576
- UPDATE_BALANCE_VALIDATOR_VERSION: 1,
577
- PROCESS_PAYMENT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
578
- PROCESS_PAYMENT_VALIDATOR_VERSION: 1,
579
- EXTEND_POSITION_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
580
- EXTEND_POSITION_VALIDATOR_VERSION: 1,
581
- POSITION_MANAGER_CORE_MODULE: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
582
- LOAN_OPERATIONS_MANAGER_MODULE: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
583
- TERM_MANAGER_MODULE: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
584
- COMMUNITY_MANAGER_MODULE: ""
585
561
  }
586
562
  };
587
563
  var ALL_DEPLOYMENTS = {