@gvnrdao/dh-sdk 0.0.324 → 0.0.328

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 = {
@@ -1393,7 +1369,7 @@ var require_pkg_src = __commonJS({
1393
1369
  description: "Production Btc Transaction Signer",
1394
1370
  version: "1.1.1",
1395
1371
  deployed: true,
1396
- deployedAt: 1786113907707,
1372
+ deployedAt: 1786465606156,
1397
1373
  size: 67999,
1398
1374
  hash: "83f3644c68819215e05ba8db3225571354a009458fc1437f606df6cb9142d3f9"
1399
1375
  },
@@ -1406,7 +1382,7 @@ var require_pkg_src = __commonJS({
1406
1382
  description: "Production Ucd Mint Validator",
1407
1383
  version: "1.2.1",
1408
1384
  deployed: true,
1409
- deployedAt: 1786113908489,
1385
+ deployedAt: 1786465627201,
1410
1386
  size: 82739,
1411
1387
  hash: "2bdd9aebdaad58a43a4731d5b7284f2531b7a9caaf8caf5719d891c4ed33cb2e",
1412
1388
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
@@ -1424,7 +1400,7 @@ var require_pkg_src = __commonJS({
1424
1400
  description: "Production Process Payment Validator",
1425
1401
  version: "1.2.1",
1426
1402
  deployed: true,
1427
- deployedAt: 1786113909138,
1403
+ deployedAt: 1786465654796,
1428
1404
  size: 78736,
1429
1405
  hash: "17191ecc9c009e0fa5ab52c876330beb2a9a75d30cccb92e7c0c04f169df088b",
1430
1406
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
@@ -1442,7 +1418,7 @@ var require_pkg_src = __commonJS({
1442
1418
  description: "Production Extend Position Validator",
1443
1419
  version: "1.2.1",
1444
1420
  deployed: true,
1445
- deployedAt: 1786113909872,
1421
+ deployedAt: 1786465681874,
1446
1422
  size: 76515,
1447
1423
  hash: "1d4e085335af80f194a663a577876902c83015177e95906a0b461fa93bd5da08",
1448
1424
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
@@ -1452,21 +1428,17 @@ var require_pkg_src = __commonJS({
1452
1428
  }
1453
1429
  },
1454
1430
  btcWithdrawal: {
1455
- cid: "QmZuR3qSBajutXfRd91ffpyFrRMgmaMh7fDEHRiQQF8Ljp",
1431
+ cid: "QmRYVoTTDxk2McvBqxSm9M7YcaBGdTqjnZGPX4Y191DaZh",
1456
1432
  authorizedCidHex: cidToHex(
1457
- "QmZuR3qSBajutXfRd91ffpyFrRMgmaMh7fDEHRiQQF8Ljp"
1433
+ "QmRYVoTTDxk2McvBqxSm9M7YcaBGdTqjnZGPX4Y191DaZh"
1458
1434
  ),
1459
1435
  name: "Btc Withdrawal",
1460
1436
  description: "Production Btc Withdrawal",
1461
- // Monotonic per action, NOT a shared release train — the EIP-1271 rotation
1462
- // stamped its cohort 1.2.1, but this action was already at 1.2.3 from the vout
1463
- // canonicalization fix. Reusing the cohort stamp made a newer CID read as older
1464
- // than the one it replaced, which a rotation ceremony would treat as a rollback.
1465
- version: "1.2.4",
1437
+ version: "1.2.7",
1466
1438
  deployed: true,
1467
- deployedAt: 1786113910531,
1468
- size: 95313,
1469
- hash: "ce12f11e3b5469953bc919586e4f3b8b5d594224b58b799dd9a3b60e3749559c",
1439
+ deployedAt: 1786446046614,
1440
+ size: 97456,
1441
+ hash: "3cae8250861832f91ee3c573abeb50ff27b9acd34148176071cbfbabb359fc91",
1470
1442
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1471
1443
  pkp: {
1472
1444
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1578,19 +1550,17 @@ var require_pkg_src = __commonJS({
1578
1550
  hash: "03ee5be239c91c735d2c95db89a0d1b77e602cfc18af8eb64b3281fddcd90a72"
1579
1551
  },
1580
1552
  agentModuleExecutor: {
1581
- // Bound to the isolated Sepolia agent group 7 ("DH-Agent-Sepolia"), which
1582
- // holds this action and no PKPs until an agent is minted into it. NOT yet
1583
- // bound on mainnet — the Base `DH-Agent-Mainnet` group is created at rollout.
1584
- cid: "QmVgjGXUuxoGf8XyddW8a7btaazT9yEotpKoBgb65oPvcF",
1553
+ cid: "QmdhC18wF7PFGMQ8aThdPsEVSpuDewA27dFvbA3bG6kVDQ",
1585
1554
  authorizedCidHex: cidToHex(
1586
- "QmVgjGXUuxoGf8XyddW8a7btaazT9yEotpKoBgb65oPvcF"
1555
+ "QmdhC18wF7PFGMQ8aThdPsEVSpuDewA27dFvbA3bG6kVDQ"
1587
1556
  ),
1588
1557
  name: "Agent Module Executor",
1589
- description: "Capability gate for multi-sig Safe agent delegation: binds the agent PKP to the AgentDelegationRegistry record before signing (authorize | execute modes)",
1590
- version: "1.0.0",
1558
+ description: "Production Agent Module Executor",
1559
+ version: "1.1.1",
1591
1560
  deployed: true,
1592
- size: 11785,
1593
- hash: "fa294e164273c2158f9ef11a0ccfd735c343c0e3759fcb4b37e0f8a5d1ab0d29"
1561
+ deployedAt: 1786446108348,
1562
+ size: 11655,
1563
+ hash: "d4e2149f843d6c2699fc61114d54b268ba4ed2c46ebdaee1145e0f101ec2a1ec"
1594
1564
  },
1595
1565
  staleSpendRecoveryValidator: {
1596
1566
  cid: "QmXQWT8V4wjuvH8BbC7xrkYxsVu5AKfvfWcS7jnYfrexYm",
@@ -1619,7 +1589,7 @@ var require_pkg_src = __commonJS({
1619
1589
  description: "Production Btc Utxo Invalidator",
1620
1590
  version: "1.0.0",
1621
1591
  deployed: true,
1622
- deployedAt: 1786113911948,
1592
+ deployedAt: 1786465704240,
1623
1593
  size: 62553,
1624
1594
  hash: "1dcea2b49e545c423195be22e072dc5e3039ed1c95a8490b23505956029f5b33"
1625
1595
  }
@@ -5528,8 +5498,14 @@ function getMainnetConfig() {
5528
5498
  agentModuleFactory: MAINNET_CONTRACTS.AgentModuleFactory || ""
5529
5499
  },
5530
5500
  subgraphs: {
5501
+ // KNOWN-WRONG placeholder (see GOAL-PLAN step 2 / dogfood F-CLI-4): this is a
5502
+ // SEPOLIA subgraph id in a mainnet (chainId 1) config, and this gateway host
5503
+ // needs an Authorization key the SDK does not (and must not) hold — so the
5504
+ // URL is doubly unusable. The `CCTPsd…` path segment is the subgraph's
5505
+ // PUBLIC ID, not a credential. The fix is a chain-correct SERVER-side proxy
5506
+ // (see mcp/docs/SUBGRAPH-PROXY-PLAN.md) — same rule as rpcUrls above:
5507
+ // never ship an API-keyed URL in a client config.
5531
5508
  diamondHandsUrl: "https://gateway-arbitrum.network.thegraph.com/api/subgraphs/id/CCTPsdYqco2jChDLLBQTbdJWwoukVoMt1cXeR9ti6r9A"
5532
- // Published Sepolia subgraph on Arbitrum One
5533
5509
  },
5534
5510
  litNetwork: "chipotle",
5535
5511
  debug: false
@@ -6326,7 +6302,7 @@ async function generateMintAuthorization(positionId, amount, chainId, signerOrPr
6326
6302
  signature
6327
6303
  };
6328
6304
  }
6329
- async function generatePaymentAuthorization(positionId, amount, chainId, signer) {
6305
+ async function generatePaymentAuthorization(positionId, amount, chainId, signerOrPrecomputed) {
6330
6306
  if (typeof positionId !== "string") {
6331
6307
  throw new Error(`positionId must be a string, got ${typeof positionId}`);
6332
6308
  }
@@ -6336,6 +6312,18 @@ async function generatePaymentAuthorization(positionId, amount, chainId, signer)
6336
6312
  if (typeof chainId !== "number") {
6337
6313
  throw new Error(`chainId must be a number, got ${typeof chainId}`);
6338
6314
  }
6315
+ if (typeof signerOrPrecomputed === "object" && "signature" in signerOrPrecomputed && !("signMessage" in signerOrPrecomputed)) {
6316
+ const { timestamp: timestamp2, signature: signature2 } = signerOrPrecomputed;
6317
+ return {
6318
+ positionId,
6319
+ timestamp: timestamp2,
6320
+ chainId,
6321
+ amount: amount.toString(),
6322
+ action: "make-payment",
6323
+ signature: signature2
6324
+ };
6325
+ }
6326
+ const signer = signerOrPrecomputed;
6339
6327
  const timestamp = await calculateNextQuantumTimestamp();
6340
6328
  if (typeof timestamp !== "number") {
6341
6329
  throw new Error(`timestamp must be a number, got ${typeof timestamp}`);
@@ -6577,6 +6565,26 @@ async function generateExtendAuthorization(positionId, newTerm, chainId, signerO
6577
6565
  };
6578
6566
  }
6579
6567
 
6568
+ // src/utils/authorization-provider.utils.ts
6569
+ async function resolveAuthorizationInput(provider, signer, ctx) {
6570
+ if (!provider)
6571
+ return signer;
6572
+ const result = await provider(ctx);
6573
+ if (!result || typeof result.signature !== "string" || result.signature.length === 0) {
6574
+ throw new Error(
6575
+ "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"
6576
+ );
6577
+ }
6578
+ if (!Number.isInteger(result.timestamp)) {
6579
+ throw new Error(
6580
+ `authorizationProvider returned a non-integer timestamp (${String(
6581
+ result.timestamp
6582
+ )}) \u2014 the protocol call must carry the same value the envelope was signed over`
6583
+ );
6584
+ }
6585
+ return { timestamp: result.timestamp, signature: result.signature };
6586
+ }
6587
+
6580
6588
  // src/utils/eip712-login.ts
6581
6589
  import { hexlify, randomBytes } from "ethers";
6582
6590
  function buildLoginDomain(chainId) {
@@ -20002,11 +20010,20 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
20002
20010
  {}
20003
20011
  );
20004
20012
  }
20013
+ const mintAuthInput = await resolveAuthorizationInput(
20014
+ request.authorizationProvider,
20015
+ signer2,
20016
+ {
20017
+ positionId: request.positionId,
20018
+ timestamp: calculateNextQuantumTimestamp(),
20019
+ amount: requestAmountWei.toString()
20020
+ }
20021
+ );
20005
20022
  const auth = await generateMintAuthorization(
20006
20023
  request.positionId,
20007
20024
  BigInt(requestAmountWei.toString()),
20008
20025
  chainId,
20009
- signer2
20026
+ mintAuthInput
20010
20027
  );
20011
20028
  if (this.config.debug) {
20012
20029
  log.info(` Authorization generated (attempt ${attempt})`, {});
@@ -21597,7 +21614,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21597
21614
  * @param _selectedTerm - Extension term in months
21598
21615
  * @returns Transaction receipt
21599
21616
  */
21600
- async extendPosition(_positionId, _selectedTerm, _rpcUrl, _bitcoinRpcUrl) {
21617
+ async extendPosition(_positionId, _selectedTerm, _rpcUrl, _bitcoinRpcUrl, _authorizationProvider) {
21601
21618
  this.ensureInitialized();
21602
21619
  if (typeof _positionId !== "string") {
21603
21620
  throw new Error(`positionId must be a string, got ${typeof _positionId}`);
@@ -21691,11 +21708,23 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21691
21708
  }
21692
21709
  }
21693
21710
  const signer = this.getSignerOrThrow();
21711
+ const extendAuthInput = await resolveAuthorizationInput(
21712
+ _authorizationProvider,
21713
+ signer,
21714
+ {
21715
+ positionId: _positionId,
21716
+ timestamp: calculateNextQuantumTimestamp(),
21717
+ // The extend envelope binds the TERM, not an amount. Carried as "0" so
21718
+ // the context shape stays uniform across the four operations.
21719
+ amount: "0",
21720
+ selectedTerm: _selectedTerm
21721
+ }
21722
+ );
21694
21723
  const auth = await generateExtendAuthorization(
21695
21724
  _positionId,
21696
21725
  _selectedTerm,
21697
21726
  chainId,
21698
- signer
21727
+ extendAuthInput
21699
21728
  );
21700
21729
  if (this.config.debug) {
21701
21730
  log.info(" Extend authorization generated", {
@@ -21951,7 +21980,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21951
21980
  positionId,
21952
21981
  selectedTerm,
21953
21982
  request.rpcUrl,
21954
- request.customBitcoinRpcUrl
21983
+ request.customBitcoinRpcUrl,
21984
+ request.authorizationProvider
21955
21985
  );
21956
21986
  let newExpiryAt;
21957
21987
  try {
@@ -22479,11 +22509,20 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22479
22509
  }
22480
22510
  let auth;
22481
22511
  try {
22512
+ const paymentAuthInput = await resolveAuthorizationInput(
22513
+ request.authorizationProvider,
22514
+ signer,
22515
+ {
22516
+ positionId: request.positionId,
22517
+ timestamp: calculateNextQuantumTimestamp(),
22518
+ amount: paymentAmountWei.toString()
22519
+ }
22520
+ );
22482
22521
  auth = await generatePaymentAuthorization(
22483
22522
  request.positionId,
22484
22523
  paymentAmountWei,
22485
22524
  chainId,
22486
- signer
22525
+ paymentAuthInput
22487
22526
  );
22488
22527
  } catch (authError) {
22489
22528
  if (authError.message && authError.message.includes("BigNumber")) {
@@ -22995,7 +23034,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22995
23034
  * @param customBitcoinRpcUrl - Optional custom Bitcoin RPC URL (for local testing)
22996
23035
  * @returns Withdrawal result with transaction details
22997
23036
  */
22998
- async withdrawBTC(positionId, withdrawalAddress, withdrawalAmount) {
23037
+ async withdrawBTC(positionId, withdrawalAddress, withdrawalAmount, options) {
22999
23038
  this.ensureInitialized();
23000
23039
  try {
23001
23040
  if (this.config.debug) {
@@ -23210,11 +23249,21 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
23210
23249
  {}
23211
23250
  );
23212
23251
  }
23252
+ const withdrawAuthInput = await resolveAuthorizationInput(
23253
+ options?.authorizationProvider,
23254
+ signer,
23255
+ {
23256
+ positionId,
23257
+ timestamp: calculateNextQuantumTimestamp(),
23258
+ amount: BigInt(withdrawalAmount).toString(),
23259
+ destinationAddress: withdrawalAddress
23260
+ }
23261
+ );
23213
23262
  const auth = await generateWithdrawAuthorization(
23214
23263
  positionId,
23215
23264
  BigInt(withdrawalAmount),
23216
23265
  chainId,
23217
- signer,
23266
+ withdrawAuthInput,
23218
23267
  withdrawalAddress
23219
23268
  // Pass destination address for message hash
23220
23269
  );
@@ -24288,6 +24337,11 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24288
24337
  * (`MAX_ATTESTATION_AGE = 1 hour`) and replay-protects via
24289
24338
  * `authorizedAt` binding.
24290
24339
  *
24340
+ * WHO PAYS: in **service mode** lit-ops-server performs BOTH steps and its
24341
+ * relayer funds the transaction — the borrower is never prompted, because the
24342
+ * Bitcoin withdrawal this cleans up after has already settled. In **direct
24343
+ * mode** (CLI / tests / hardhat) the SDK's own signer submits step 2.
24344
+ *
24291
24345
  * `invalidatorTxid` is either:
24292
24346
  * - the borrower's own authorized broadcast (classified "consumed-as-authorized"
24293
24347
  * by the LIT validator), once it has ≥6 confirmations on the BTC
@@ -24300,7 +24354,6 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24300
24354
  async recoverStaleSpend(params) {
24301
24355
  this.ensureInitialized();
24302
24356
  try {
24303
- const signer = this.getSignerOrThrow();
24304
24357
  const contractAddresses = this.getContractAddressesOrThrow();
24305
24358
  const btcSpendAuthorizerAddress = contractAddresses.btcSpendAuthorizer;
24306
24359
  if (!btcSpendAuthorizerAddress) {
@@ -24346,6 +24399,28 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24346
24399
  success: false,
24347
24400
  error: json.error ?? "stale-spend recovery request failed"
24348
24401
  };
24402
+ if (!attRes.success || !attRes.attestation) {
24403
+ return {
24404
+ success: false,
24405
+ failedStep: "attestation",
24406
+ error: attRes.error ?? attRes.reason ?? `LIT recovery attestation rejected${attRes.failedStep ? ` at step ${attRes.failedStep}` : ""}`
24407
+ };
24408
+ }
24409
+ if (!attRes.transactionHash && !attRes.alreadyCleared) {
24410
+ return {
24411
+ success: false,
24412
+ failedStep: "submit",
24413
+ 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."
24414
+ };
24415
+ }
24416
+ return {
24417
+ success: true,
24418
+ ...attRes.transactionHash && {
24419
+ transactionHash: attRes.transactionHash
24420
+ },
24421
+ ...attRes.classification && { classification: attRes.classification },
24422
+ attestation: attRes.attestation
24423
+ };
24349
24424
  } else {
24350
24425
  const rpcUrl = params.rpcUrl ?? this.config.ethRpcUrl;
24351
24426
  if (!rpcUrl) {
@@ -24355,6 +24430,13 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24355
24430
  error: "ethRpcUrl required for recoverStaleSpend \u2014 set DiamondHandsSDKConfig.ethRpcUrl or pass rpcUrl"
24356
24431
  };
24357
24432
  }
24433
+ if (!this.config.contractSigner) {
24434
+ return {
24435
+ success: false,
24436
+ failedStep: "config",
24437
+ error: "Contract signer not configured \u2014 recoverStaleSpend submits its own recovery transaction in direct mode. Set DiamondHandsSDKConfig.contractSigner, or use service mode, where the server relays and pays for it."
24438
+ };
24439
+ }
24358
24440
  const sdkBitcoinProviderUrl = this.config.bitcoinProviders?.[0]?.url;
24359
24441
  attRes = await this.litOps.requestStaleSpendRecoveryAttestation({
24360
24442
  chainId,
@@ -24395,7 +24477,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24395
24477
  [
24396
24478
  "function cancelStaleSpendWithProof(bytes32 positionId, bytes32 utxoKey, uint256 authorizedAt, string calldata invalidatorTxid, uint256 attestationTimestamp, bytes calldata litSignature) external"
24397
24479
  ],
24398
- signer
24480
+ this.getSignerOrThrow()
24399
24481
  );
24400
24482
  const tx = await btcSpendAuthorizer["cancelStaleSpendWithProof"](
24401
24483
  attRes.attestation.positionId,
@@ -25203,10 +25285,22 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25203
25285
  "getLoanGrant",
25204
25286
  data
25205
25287
  );
25288
+ const raw = getBytes5(data);
25289
+ const withdrawScopeSupported = raw.length >= 128;
25290
+ const minWithdrawRatioBps = withdrawScopeSupported ? Number(
25291
+ AbiCoder2.defaultAbiCoder().decode(["uint32"], raw.slice(96, 128))[0]
25292
+ ) : 0;
25206
25293
  return {
25207
25294
  borrower,
25208
25295
  scopeBits: Number(scopeBits),
25209
- minCollateralRatioBps: Number(minCollateralRatioBps)
25296
+ minCollateralRatioBps: Number(minCollateralRatioBps),
25297
+ minWithdrawRatioBps,
25298
+ /**
25299
+ * True when the deployed registry is v2.1.0+ and therefore honours `SCOPE_WITHDRAW`. The
25300
+ * frontend gates its Withdrawal control on this so the control is absent — not disabled,
25301
+ * not "coming soon" — on a chain whose registry cannot support it.
25302
+ */
25303
+ withdrawScopeSupported
25210
25304
  };
25211
25305
  }
25212
25306
  /**
@@ -25248,10 +25342,14 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25248
25342
  const mintEnabled = hasGrant && (scopes & 4) !== 0;
25249
25343
  const floor = grant.minCollateralRatioBps;
25250
25344
  const agent = user ? await registry.agentOf(user) : null;
25345
+ const withdrawEnabled = hasGrant && grant.withdrawScopeSupported && (scopes & 8) !== 0;
25251
25346
  return {
25252
25347
  renewEnabled: hasGrant && (scopes & 1) !== 0,
25253
25348
  repayEnabled: hasGrant && (scopes & 2) !== 0,
25254
25349
  mintEnabled,
25350
+ withdrawEnabled,
25351
+ withdrawScopeSupported: grant.withdrawScopeSupported,
25352
+ minWithdrawRatioBps: withdrawEnabled ? grant.minWithdrawRatioBps : 0,
25255
25353
  minCollateralRatioBps: mintEnabled ? floor : 0,
25256
25354
  effectiveMintFloorBps: mintEnabled ? Math.max(floor, protocolMin) : 0,
25257
25355
  protocolMinFloorBps: protocolMin,
@@ -25437,8 +25535,83 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25437
25535
  const receipt = await tx.wait();
25438
25536
  return { hash: tx.hash, blockNumber: receipt?.blockNumber ?? 0 };
25439
25537
  }
25440
- /** Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on. */
25441
- async assertMintFloorAtOrAboveProtocolMin(floorBps) {
25538
+ /**
25539
+ * Grant the agent the WITHDRAW scope for a position (registry v2.1.0+). Borrower-signed.
25540
+ *
25541
+ * CONTRACT WALLETS ONLY, and this is enforced here rather than on-chain. The withdrawal
25542
+ * validator's only non-borrower arm is `checkSafeAgentWithdrawAuthorization`, which requires a
25543
+ * factory-provenanced `AgentModule` whose `safe()` is the borrower — something an EOA cannot
25544
+ * have. The registry deliberately does NOT check wallet type ("Callable by any borrower (this
25545
+ * function does not itself require a Safe)"), so an EOA can record a grant that no code path
25546
+ * will ever satisfy. Refusing here keeps the supported path from writing a dead record.
25547
+ *
25548
+ * That refusal is ergonomics, NOT enforcement — `enableAutoWithdraw` is `external` and reachable
25549
+ * from any wallet. The real reason delegated withdrawal is closed to EOAs is that they can sign
25550
+ * inside the ~44s window themselves, so delegating buys them nothing and only adds risk.
25551
+ *
25552
+ * @param minWithdrawRatioBps Post-withdrawal collateral-ratio floor for AGENT withdrawals.
25553
+ * Clamped by the same governance minimum as the mint floor. **Inert at zero debt** — it
25554
+ * is a ratio, and the post-withdrawal ratio is infinite for any amount once debt is 0.
25555
+ * The operative bound there is the quorum-gated destination allowlist. Do not surface
25556
+ * this to users as a cap on how much an agent may withdraw.
25557
+ * There is no in-place raise: changing it means disable-then-re-enable, deliberately —
25558
+ * on the one scope that moves custodied BTC, incident response can only disable outright.
25559
+ */
25560
+ async enableAutoWithdraw(positionId, minWithdrawRatioBps, options) {
25561
+ const pid = this.toBytes32(positionId);
25562
+ await this.assertBorrowerIsContractWallet(pid);
25563
+ await this.assertMintFloorAtOrAboveProtocolMin(minWithdrawRatioBps, "withdraw");
25564
+ const { registry, agentAddress } = await this.prepareAgentDelegation(pid, options);
25565
+ const tx = await registry.enableAutoWithdraw(pid, minWithdrawRatioBps);
25566
+ const receipt = await tx.wait();
25567
+ return { hash: tx.hash, blockNumber: receipt?.blockNumber ?? 0, agentAddress };
25568
+ }
25569
+ /**
25570
+ * Revoke the WITHDRAW scope for a position (also zeroes its withdraw floor). Borrower-signed.
25571
+ *
25572
+ * No wallet-type check: revoking is the safe direction, and an EOA that recorded a grant before
25573
+ * this guard existed must be able to clear it.
25574
+ */
25575
+ async disableAutoWithdraw(positionId) {
25576
+ const addr = this.agentDelegationRegistryOrThrow();
25577
+ const signer = this.getSignerOrThrow();
25578
+ const registry = AgentDelegationRegistry__factory.connect(addr, signer);
25579
+ const pid = this.toBytes32(positionId);
25580
+ const tx = await registry.disableAutoWithdraw(pid);
25581
+ const receipt = await tx.wait();
25582
+ await this.clearGate1IfNoScopesRemain(pid);
25583
+ return { hash: tx.hash, blockNumber: receipt?.blockNumber ?? 0 };
25584
+ }
25585
+ /**
25586
+ * Layer 2 of the withdrawal-delegation gating: refuse to write a grant that could never be
25587
+ * satisfied. See `enableAutoWithdraw` for why this lives here and not in the registry.
25588
+ */
25589
+ async assertBorrowerIsContractWallet(pid) {
25590
+ const details = await this.getPositionDetailsView(pid);
25591
+ const borrower = details?.borrower;
25592
+ if (!borrower) {
25593
+ throw new SDKError({
25594
+ message: `Could not resolve the borrower for position ${pid} \u2014 refusing to write a delegation grant against an unverified position`,
25595
+ category: "VALIDATION" /* VALIDATION */,
25596
+ severity: "HIGH" /* HIGH */
25597
+ });
25598
+ }
25599
+ const code = await this.getProviderOrThrow().getCode(borrower);
25600
+ if (!code || code === "0x") {
25601
+ throw new SDKError({
25602
+ 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.`,
25603
+ category: "VALIDATION" /* VALIDATION */,
25604
+ severity: "MEDIUM" /* MEDIUM */
25605
+ });
25606
+ }
25607
+ }
25608
+ /**
25609
+ * Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on.
25610
+ *
25611
+ * `agentMintFloorMinBps` is the governance floor-of-floors for BOTH the mint and withdraw
25612
+ * grants — one lever, one invariant — so `scope` only shapes the message.
25613
+ */
25614
+ async assertMintFloorAtOrAboveProtocolMin(floorBps, scope = "mint") {
25442
25615
  if (!Number.isInteger(floorBps) || floorBps <= 0) {
25443
25616
  throw new SDKError({
25444
25617
  message: `Invalid collateral floor: ${floorBps} bps \u2014 must be a positive integer`,
@@ -25449,7 +25622,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
25449
25622
  const protocolMin = await this.getAgentMintFloorMinBps();
25450
25623
  if (protocolMin === 0) {
25451
25624
  throw new SDKError({
25452
- message: "Agent minting is not configured on this network (agentMintFloorMinBps is 0) \u2014 the registry would reject every mint grant",
25625
+ message: `Agent ${scope === "mint" ? "minting" : "withdrawal"} is not configured on this network (agentMintFloorMinBps is 0) \u2014 the registry would reject every ${scope} grant`,
25453
25626
  category: "CONFIGURATION" /* CONFIGURATION */,
25454
25627
  severity: "HIGH" /* HIGH */
25455
25628
  });
@@ -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