@gvnrdao/dh-sdk 0.0.301 → 0.0.304

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.js CHANGED
@@ -87,7 +87,8 @@ var init_deployment_addresses = __esm({
87
87
  mockUsdtToken: "0xF8b7fB38b17a310960E47805Be9adba0b0e0394D",
88
88
  PositionDelegateRegistry: "0x482369De283622E7b05731875ec137d4E3D208F9",
89
89
  FeeRecipientRegistry: "0x95795F8403DDb629E8527B2934C4e71f5fC0C374",
90
- BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe"
90
+ BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe",
91
+ CarryAgentRegistry: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7"
91
92
  },
92
93
  upgraded: {
93
94
  PositionManager: {
@@ -349,7 +350,9 @@ var init_deployment_addresses = __esm({
349
350
  FEE_RECIPIENT_REGISTRY_IMPL: "0xd08513a7cB2dd354B26cDDf549616F12054c34c9",
350
351
  OPERATION_AUTHORIZATION_REGISTRY_IMPL: "0xe0D981B60351f0f8fFDA2b50C8180Fb37CeF3883",
351
352
  BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe",
352
- BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0xFF270BC4470dBA80430Bd1a3a66EB691599354fA"
353
+ BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0xFF270BC4470dBA80430Bd1a3a66EB691599354fA",
354
+ CARRY_AGENT_REGISTRY: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7",
355
+ CARRY_AGENT_REGISTRY_IMPL: "0x1b11a3D5e46F3672E643c997f0d569DFf0b64a01"
353
356
  }
354
357
  };
355
358
  SEPOLIA_CONTRACTS = {
@@ -377,7 +380,8 @@ var init_deployment_addresses = __esm({
377
380
  mockUsdtToken: "0xF8b7fB38b17a310960E47805Be9adba0b0e0394D",
378
381
  PositionDelegateRegistry: "0x482369De283622E7b05731875ec137d4E3D208F9",
379
382
  FeeRecipientRegistry: "0x95795F8403DDb629E8527B2934C4e71f5fC0C374",
380
- BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe"
383
+ BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe",
384
+ CarryAgentRegistry: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7"
381
385
  };
382
386
  MAINNET_DEPLOYMENT = {
383
387
  network: "mainnet",
@@ -412,10 +416,12 @@ var init_deployment_addresses = __esm({
412
416
  upgraded: {
413
417
  SimplePSMV2: {
414
418
  previousImplementation: "0x1a555FB8D6EDdF755a4575E541651EdB8B49a15A",
415
- newImplementation: "0x1a555FB8D6EDdF755a4575E541651EdB8B49a15A",
416
- upgradedAt: "2026-06-16T17:53:24.979Z",
417
- upgradedBy: "0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f",
418
- reason: "Initial PSM deploy / artifact sync"
419
+ newImplementation: "0x8717761444b1822c2E6916aB5bD9ac96c91b6DF5",
420
+ upgradedAt: "2026-07-06T10:41:35.000Z",
421
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x5052d1559d56b8e178699183e56c4fd84a163e987365c5af9c8d669a723c35d6 (block 25472960)",
422
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
423
+ executeTx: "0x5052d1559d56b8e178699183e56c4fd84a163e987365c5af9c8d669a723c35d6",
424
+ block: 25472960
419
425
  },
420
426
  CollateralManagerModule: {
421
427
  previousImplementation: "0x1919860e528Cd2AbbD31a17858c986b8aCc8C185",
@@ -430,6 +436,65 @@ var init_deployment_addresses = __esm({
430
436
  upgradedAt: "2026-07-08T20:33:59Z",
431
437
  upgradedBy: "AdminModule generic-op 0x2603a486c0f34554b864cc1d909b325c7e105331da10ec0fd7e0b9373fa76207 (3-of-5 ADMIN_ROLE Safes, 24h timelock; executed by 0xfb3B8cbeFB7D666A6Af34C53e189EdC31862a7be in tx 0x430c8e450f12d3d9359cde356ac5e2ef39661d7a89884b5674a596c1090f6cff)",
432
438
  reason: "Validator gas fix (#420): BitcoinAddressValidator packed-table lookups; VERSION 1.0.0 -> 1.0.1"
439
+ },
440
+ UCDController: {
441
+ newImplementation: "0x28B8cAB89D89B3F83581FD2290de2Ce84535bd0A",
442
+ upgradedBy: "Reconciled from chain \u2014 Upgraded event log unavailable; provenance omitted (never fabricated)",
443
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)"
444
+ },
445
+ PositionManagerCoreModule: {
446
+ previousImplementation: "0x6C8b0FB93F8d0239254aefaC24d4264cA2F8AAeB",
447
+ newImplementation: "0x5067Ea1d40b2Aaf4ad693bD9b6e7463C1A29E849",
448
+ upgradedAt: "2026-07-06T08:02:35.000Z",
449
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0x2002827d34A781883Ec4c83c36ECC140c3911F2B in tx 0xeb2bfe77d46394b6d0afd46beb0a80cd9beb91f6771ccb59ff9fba27d4af4f3d (block 25472167)",
450
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
451
+ executeTx: "0xeb2bfe77d46394b6d0afd46beb0a80cd9beb91f6771ccb59ff9fba27d4af4f3d",
452
+ block: 25472167
453
+ },
454
+ LiquidationManagerModule: {
455
+ previousImplementation: "0xc2f4517eeD7c4c516939a64c7938a6ab432D99c4",
456
+ newImplementation: "0xe7444C1d9652dAb31BaAdBE4C7c7e7Df3515dA46",
457
+ upgradedAt: "2026-07-06T10:40:47.000Z",
458
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0xc173b97af68cc9172ea0ddc91a2c90bf02eac1505e749d92aa044aa69891f48c (block 25472956)",
459
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
460
+ executeTx: "0xc173b97af68cc9172ea0ddc91a2c90bf02eac1505e749d92aa044aa69891f48c",
461
+ block: 25472956
462
+ },
463
+ BTCSpendAuthorizer: {
464
+ previousImplementation: "0xb003ed89F695E8819a85513fE17260b805B3c478",
465
+ newImplementation: "0x40c9e8C46C65E0A57BB63da5368868A9E8Df81A5",
466
+ upgradedAt: "2026-07-06T10:40:59.000Z",
467
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0xe1b1884a7e76c7fd569522545a04e49d5128d83d27b3892cda59f143b53b5281 (block 25472957)",
468
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
469
+ executeTx: "0xe1b1884a7e76c7fd569522545a04e49d5128d83d27b3892cda59f143b53b5281",
470
+ block: 25472957
471
+ },
472
+ PositionManager: {
473
+ previousImplementation: "0x9f52BaBB40FbFC5863e45B1A4014cB433950c684",
474
+ newImplementation: "0x600Dc0D1C2c0F94882B6Bc09172f8650047Ad5B1",
475
+ upgradedAt: "2026-07-06T08:42:47.000Z",
476
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0x2002827d34A781883Ec4c83c36ECC140c3911F2B in tx 0xa1fbe31cb71736514a868e681573dff22c9fab91d69cec157a24a30e29eadbe6 (block 25472368)",
477
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
478
+ executeTx: "0xa1fbe31cb71736514a868e681573dff22c9fab91d69cec157a24a30e29eadbe6",
479
+ block: 25472368
480
+ },
481
+ LoanOperationsManagerModule: {
482
+ previousImplementation: "0x56671D671e492Dae625413d463D47605Cf54edf6",
483
+ newImplementation: "0x7d90aAB4466f59BA9aC9C7a9BF14dC53734336e7",
484
+ upgradedAt: "2026-07-06T10:41:23.000Z",
485
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x327ae9f7a3917788d22dfe95611e1afcd18583f210b03a119d9f04eca50b9e7a (block 25472959)",
486
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
487
+ executeTx: "0x327ae9f7a3917788d22dfe95611e1afcd18583f210b03a119d9f04eca50b9e7a",
488
+ block: 25472959
489
+ },
490
+ OperationAuthorizationRegistry: {
491
+ previousImplementation: "0x327662917Ac8EEda47851d74AF6A151348636fCe",
492
+ newImplementation: "0x493118746840EDB2D396E71632D2E774CA14070e",
493
+ upgradedAt: "2026-07-06T10:41:47.000Z",
494
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x9820e259700865a281b8376e248044048ba2fed1d17c811ac66ac54d36711a5b (block 25472961)",
495
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
496
+ executeTx: "0x9820e259700865a281b8376e248044048ba2fed1d17c811ac66ac54d36711a5b",
497
+ block: 25472961
433
498
  }
434
499
  },
435
500
  latestEnv: {
@@ -442,9 +507,37 @@ var init_deployment_addresses = __esm({
442
507
  BITCOIN_PROVIDER_REGISTRY: "0x4ab3Fb96e02893Ed6FAa61bad1918390c8DbbFFC",
443
508
  BITCOIN_PROVIDER_REGISTRY_IMPL: "0x7Bc9473E45fb3EA594e8b13992F57D0444902ddB",
444
509
  BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY: "0xf667f77FDba7458345f227eB6Bfe4148243E7270",
445
- BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0x11731bFEC3068F2cc19D016381dfEA3B93013b1C",
510
+ BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0x190c08300Ae650fb88ed8156445BBeb13bE9E8a3",
446
511
  SIMPLE_PSM_V2: "0xfE0C14B4E614f40Bf58845b4B04CeD17fd116532",
447
- SIMPLE_PSM_V2_IMPL: "0x1a555FB8D6EDdF755a4575E541651EdB8B49a15A"
512
+ SIMPLE_PSM_V2_IMPL: "0x8717761444b1822c2E6916aB5bD9ac96c91b6DF5",
513
+ UCD_CONTROLLER: "0x4b4f8063048bCD59D469219E3f07C6aaDF4f6157",
514
+ UCD_CONTROLLER_IMPL: "0x28B8cAB89D89B3F83581FD2290de2Ce84535bd0A",
515
+ UCD_TOKEN: "0xaAaB6B05E5EecB015898483d4e6703d7F218850b",
516
+ UCD_TOKEN_IMPL: "0x5f09C660036021D7e916DF47234C24a2536C3A72",
517
+ POSITION_MANAGER_CORE_MODULE: "0xd895B685914c1BE4e1c7aab9f89a90c203DE8d22",
518
+ POSITION_MANAGER_CORE_MODULE_IMPL: "0x5067Ea1d40b2Aaf4ad693bD9b6e7463C1A29E849",
519
+ TERM_MANAGER_MODULE: "0xDB99CdF883ffa38acD099bC39F8d1f4508c9239E",
520
+ TERM_MANAGER_MODULE_IMPL: "0x971f63f92990e59270E971612dd399fF2C63B0c3",
521
+ COLLATERAL_MANAGER_MODULE: "0x2Eb74FEc478218f5C260aAc78Bb8eC297F9d7135",
522
+ COLLATERAL_MANAGER_MODULE_IMPL: "0x55110B9dd6B2c9f812564e7a1ef42b3c76E1A3Bb",
523
+ LIQUIDATION_MANAGER_MODULE: "0x74B28695352E7bf736D6c9F76ccb7B4439477De9",
524
+ LIQUIDATION_MANAGER_MODULE_IMPL: "0xe7444C1d9652dAb31BaAdBE4C7c7e7Df3515dA46",
525
+ CIRCUIT_BREAKER_MODULE: "0xF7b545d3a4Ee056D06909AB367E2F7bFFE3cB1BC",
526
+ CIRCUIT_BREAKER_MODULE_IMPL: "0x40e8328D61342C6ADC51dfdC723296c908f95038",
527
+ ADMIN_MODULE: "0x0023B71B586795eE5fa4E83E384345C1EC8Bb946",
528
+ ADMIN_MODULE_IMPL: "0xb4C5C7E051A1e672F014B25CB5D55B225Aa0D512",
529
+ BTC_SPEND_AUTHORIZER: "0xfBbF108486fa07F1B06a811C5b66ed68DD9c508a",
530
+ BTC_SPEND_AUTHORIZER_IMPL: "0x40c9e8C46C65E0A57BB63da5368868A9E8Df81A5",
531
+ POSITION_MANAGER_VIEWS: "0x9225647a30bbACA4Acc0491B786869036914242e",
532
+ POSITION_MANAGER_VIEWS_IMPL: "0x9225647a30bbACA4Acc0491B786869036914242e",
533
+ POSITION_MANAGER: "0x12A1836e56139ccaD8722B89519d84233efc224B",
534
+ POSITION_MANAGER_IMPL: "0x600Dc0D1C2c0F94882B6Bc09172f8650047Ad5B1",
535
+ LOAN_OPERATIONS_MANAGER_MODULE: "0x2561974A40F1AEed1251d0A090A83cF8536FB9E9",
536
+ LOAN_OPERATIONS_MANAGER_MODULE_IMPL: "0x7d90aAB4466f59BA9aC9C7a9BF14dC53734336e7",
537
+ UPGRADE_VALIDATOR: "0xC46Ae71F22e3741A6CF46E399aE914C4868fEbed",
538
+ UPGRADE_VALIDATOR_IMPL: "0x4f835e77D1211299a1cbb8Aa2b42d9B390cC95CA",
539
+ OPERATION_AUTHORIZATION_REGISTRY: "0xD4505645b724754f5170a2027804a4265e860d44",
540
+ OPERATION_AUTHORIZATION_REGISTRY_IMPL: "0x493118746840EDB2D396E71632D2E774CA14070e"
448
541
  }
449
542
  };
450
543
  MAINNET_CONTRACTS = {
@@ -475,44 +568,44 @@ var init_deployment_addresses = __esm({
475
568
  LOCALHOST_DEPLOYMENT = {
476
569
  network: "localhost",
477
570
  chainId: 31337,
478
- timestamp: "2026-03-23T15:33:34.068Z",
571
+ timestamp: "2026-06-11T12:30:59.589Z",
479
572
  deployer: "",
480
573
  contracts: {
481
- MessageHashBuilder: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
482
- UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
574
+ MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
575
+ UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
483
576
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
484
577
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
485
- PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
486
- TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
487
- LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
578
+ PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
579
+ TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
580
+ LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
488
581
  BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
489
582
  CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
490
583
  LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
491
- CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
584
+ CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
492
585
  AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
493
- PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
494
- PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB",
495
- OperationAuthorizationRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0",
496
- PKPValidation: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
586
+ PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
587
+ PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
588
+ OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
589
+ PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
497
590
  }
498
591
  };
499
592
  LOCALHOST_CONTRACTS = {
500
- MessageHashBuilder: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
501
- UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
593
+ MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
594
+ UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
502
595
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
503
596
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
504
- PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
505
- TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
506
- LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
597
+ PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
598
+ TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
599
+ LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
507
600
  BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
508
601
  CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
509
602
  LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
510
- CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
603
+ CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
511
604
  AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
512
- PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
513
- PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB",
514
- OperationAuthorizationRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0",
515
- PKPValidation: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
605
+ PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
606
+ PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
607
+ OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
608
+ PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
516
609
  };
517
610
  ALL_DEPLOYMENTS = {
518
611
  sepolia: SEPOLIA_DEPLOYMENT,
@@ -1204,30 +1297,30 @@ var require_pkg_src = __commonJS({
1204
1297
  }
1205
1298
  },
1206
1299
  btcTransactionSigner: {
1207
- cid: "QmcPG9C5Yx9JGrBKCfSVWbKuiRXn8XFwb91yNxQqY4VSJD",
1300
+ cid: "QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk",
1208
1301
  authorizedCidHex: cidToHex(
1209
- "QmcPG9C5Yx9JGrBKCfSVWbKuiRXn8XFwb91yNxQqY4VSJD"
1302
+ "QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk"
1210
1303
  ),
1211
1304
  name: "Btc Transaction Signer",
1212
1305
  description: "Production Btc Transaction Signer",
1213
- version: "1.0.1",
1306
+ version: "1.1.1",
1214
1307
  deployed: true,
1215
- deployedAt: 1783451891948,
1216
- size: 43688,
1217
- hash: "1ea0f0ac66cc68fc9b098dabb3c5e3ba96c25f13b133b95a06eac477028fdb1a"
1308
+ deployedAt: 1784316496778,
1309
+ size: 63679,
1310
+ hash: "b12aa725797af5ef07f8e6945f05741f583d6e479a4670b1121e97c58fe63191"
1218
1311
  },
1219
1312
  ucdMintValidator: {
1220
- cid: "QmPir8F8jmJ4ssQpEC3xFxkEnZxMHcgS6VxBQE1jkTLj5K",
1313
+ cid: "QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW",
1221
1314
  authorizedCidHex: cidToHex(
1222
- "QmPir8F8jmJ4ssQpEC3xFxkEnZxMHcgS6VxBQE1jkTLj5K"
1315
+ "QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW"
1223
1316
  ),
1224
1317
  name: "Ucd Mint Validator",
1225
1318
  description: "Production Ucd Mint Validator",
1226
- version: "1.0.0",
1319
+ version: "1.2.1",
1227
1320
  deployed: true,
1228
- deployedAt: 1781542207366,
1229
- size: 72074,
1230
- hash: "567a51d3beaedbda4f0af2636835ca8a2af89c2264e91f8dc31f522c1a424b7b",
1321
+ deployedAt: 1784316505431,
1322
+ size: 80381,
1323
+ hash: "132a0a8e6e9a1b85d02e00c0e57eaa371d825717255a28c9e644e4b521760026",
1231
1324
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1232
1325
  pkp: {
1233
1326
  publicKey: "0x041ab17cd91fc5c0b761eea6092d032807561b621b82c488826776e04a9158d61ba64d809a4729f0501289d980732cb06d8dfd06999dd9c8efd2f495dad78b31bb",
@@ -1235,17 +1328,17 @@ var require_pkg_src = __commonJS({
1235
1328
  }
1236
1329
  },
1237
1330
  processPaymentValidator: {
1238
- cid: "QmVq19NXJEpMMvaWQdsU5czLoqh28WGWoGgKMSYFaCmfa9",
1331
+ cid: "QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ",
1239
1332
  authorizedCidHex: cidToHex(
1240
- "QmVq19NXJEpMMvaWQdsU5czLoqh28WGWoGgKMSYFaCmfa9"
1333
+ "QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ"
1241
1334
  ),
1242
1335
  name: "Process Payment Validator",
1243
1336
  description: "Production Process Payment Validator",
1244
- version: "1.0.0",
1337
+ version: "1.2.1",
1245
1338
  deployed: true,
1246
- deployedAt: 1781542209526,
1247
- size: 68239,
1248
- hash: "ada27fea57aa3f3b861c49e75ed1f80b72500fdccc1d6e88ee1a3c1396b81867",
1339
+ deployedAt: 1784316520125,
1340
+ size: 76534,
1341
+ hash: "dbdf43d3f1636e0ee883d349ca28f24c5c38ed7e062b76ca75498aaf5fe594ea",
1249
1342
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1250
1343
  pkp: {
1251
1344
  publicKey: "0x04cfccb9b6c7addc79469c0e9fafebef4c70801302c62cdb26b610723a9b8527d23f98a40158c7340d83fd0e62d02b5ec74c12f7a86d98e8c6973b4811110f19ad",
@@ -1253,17 +1346,17 @@ var require_pkg_src = __commonJS({
1253
1346
  }
1254
1347
  },
1255
1348
  extendPositionValidator: {
1256
- cid: "QmP2u3bgKLtfqsFMp6Li72zMY8ZZH3ttAqp9W2wwonUmcb",
1349
+ cid: "QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4",
1257
1350
  authorizedCidHex: cidToHex(
1258
- "QmP2u3bgKLtfqsFMp6Li72zMY8ZZH3ttAqp9W2wwonUmcb"
1351
+ "QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4"
1259
1352
  ),
1260
1353
  name: "Extend Position Validator",
1261
1354
  description: "Production Extend Position Validator",
1262
- version: "1.0.1",
1355
+ version: "1.2.1",
1263
1356
  deployed: true,
1264
- deployedAt: 1781542211640,
1265
- size: 66543,
1266
- hash: "0db666fa2888e9acea13d8e4d79edd4ca252f5a4bb06d97e53aa14a9a34b7143",
1357
+ deployedAt: 1784316534278,
1358
+ size: 74352,
1359
+ hash: "c17bfc9abef16f2372316a14c14281e2acc98afdd1662d095360f1d1c5383f1d",
1267
1360
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1268
1361
  pkp: {
1269
1362
  publicKey: "0x04e52338f6a0c3362800f1a94c4c8f08be5030dde0fa217388c53d1c1a4790f4443c9f2886b2d2d8f4b75a9a156e9b028011b1a1cf7648da0e556aa980ec692836",
@@ -1271,17 +1364,17 @@ var require_pkg_src = __commonJS({
1271
1364
  }
1272
1365
  },
1273
1366
  btcWithdrawal: {
1274
- cid: "QmXtmLdGuEqMKKAtV7d8sHpUV32Kt74na4dRQdpWLasbHJ",
1367
+ cid: "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2",
1275
1368
  authorizedCidHex: cidToHex(
1276
- "QmXtmLdGuEqMKKAtV7d8sHpUV32Kt74na4dRQdpWLasbHJ"
1369
+ "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2"
1277
1370
  ),
1278
1371
  name: "Btc Withdrawal",
1279
1372
  description: "Production Btc Withdrawal",
1280
- version: "1.0.0",
1373
+ version: "1.2.1",
1281
1374
  deployed: true,
1282
- deployedAt: 1781542212709,
1283
- size: 79782,
1284
- hash: "e824eb473df8eba3a025ac3adc46730dbf89f90884d7c05a78cc07b90d6bc4df",
1375
+ deployedAt: 1784316549409,
1376
+ size: 90692,
1377
+ hash: "ca7276763142fe1fc812b70f6319973000faf338a91c531f898f8f6406035c64",
1285
1378
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1286
1379
  pkp: {
1287
1380
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1289,17 +1382,17 @@ var require_pkg_src = __commonJS({
1289
1382
  }
1290
1383
  },
1291
1384
  liquidationValidator: {
1292
- cid: "QmVicBXuZVY8t4DXLPjvXjzLqnBtnogAh5HT7BaL9VeDwF",
1385
+ cid: "QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx",
1293
1386
  authorizedCidHex: cidToHex(
1294
- "QmVicBXuZVY8t4DXLPjvXjzLqnBtnogAh5HT7BaL9VeDwF"
1387
+ "QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx"
1295
1388
  ),
1296
1389
  name: "Liquidation Validator",
1297
1390
  description: "Production Liquidation Validator",
1298
- version: "1.0.0",
1391
+ version: "1.2.1",
1299
1392
  deployed: true,
1300
- deployedAt: 1781542213421,
1301
- size: 58444,
1302
- hash: "0f42e9f8984433fef95875d35eb3c8d985e7ce15ad309e6aa1093d6fd1ba0bb5",
1393
+ deployedAt: 1784316560010,
1394
+ size: 66523,
1395
+ hash: "f74d0163c47d7db04d1547f72c3a9bc1e5b499669089cc6464479a361462ce81",
1303
1396
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1304
1397
  pkp: {
1305
1398
  publicKey: "0x041a241de58976c13e3d7ca5a18e494f7330151e3818706135d50176e78ffb673e58b11f0b67a87c73fade6c6b27d6e7a7377ea9f4c10bf31880f1d725f18cc6f8",
@@ -1325,17 +1418,17 @@ var require_pkg_src = __commonJS({
1325
1418
  }
1326
1419
  },
1327
1420
  loanVaultBtcBalance: {
1328
- cid: "QmUmqL4LcjDSpw2TtuWMcw8xYk8hPxgagY2qHDU8Mx1PLG",
1421
+ cid: "QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu",
1329
1422
  authorizedCidHex: cidToHex(
1330
- "QmUmqL4LcjDSpw2TtuWMcw8xYk8hPxgagY2qHDU8Mx1PLG"
1423
+ "QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu"
1331
1424
  ),
1332
1425
  name: "Loan Vault Btc Balance",
1333
1426
  description: "Production Loan Vault Btc Balance",
1334
- version: "1.0.0",
1427
+ version: "1.2.1",
1335
1428
  deployed: true,
1336
- deployedAt: 1781542215432,
1337
- size: 48397,
1338
- hash: "da3750a70e532b87f3590a3e2d8c2d51e700589f6e111bb291384ab01304af35",
1429
+ deployedAt: 1784316574669,
1430
+ size: 56477,
1431
+ hash: "917535f2911055b2ade2204fd868dd8b7294c7ade23e82aea488aeb0872e35d7",
1339
1432
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1340
1433
  pkp: {
1341
1434
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1349,17 +1442,17 @@ var require_pkg_src = __commonJS({
1349
1442
  // Requires BitcoinProviderRegistry.addAllowedCid(chainId, keccak256(utf8(cid)))
1350
1443
  // on each target chain (the action decrypts registry slots). No OAR entry.
1351
1444
  btcAddressBalance: {
1352
- cid: "QmWRDAg7ZCBFFhGfdXR7aUtQsG5qqBb1yVdLCxh8dPD7G5",
1445
+ cid: "QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA",
1353
1446
  authorizedCidHex: cidToHex(
1354
- "QmWRDAg7ZCBFFhGfdXR7aUtQsG5qqBb1yVdLCxh8dPD7G5"
1447
+ "QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA"
1355
1448
  ),
1356
1449
  name: "Btc Address Balance",
1357
1450
  description: "Address-only signed total BTC balance via registry consensus (no positionId, no price)",
1358
- version: "0.1.0",
1451
+ version: "0.2.1",
1359
1452
  deployed: true,
1360
- deployedAt: 1781542217452,
1361
- size: 25519,
1362
- hash: "000e2f8bdbccdc95b0db0fb3f12412e2e76eea16a8456d37914d324437ae291c",
1453
+ deployedAt: 1784316590403,
1454
+ size: 26986,
1455
+ hash: "49fb14265f3208916f12806df04492900bd08fb1b14d9651ee6278cd9ae4a428",
1363
1456
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1364
1457
  pkp: {
1365
1458
  publicKey: "0x0498cbb31facfa26e3ec83ccdabfe4fe341ca0282e0c3cb81da7113723b9726e1e49efa92165cc3faa66ced4454cea56dbab12267852d6525a55d7c4f128b912cd",
@@ -1393,17 +1486,17 @@ var require_pkg_src = __commonJS({
1393
1486
  hash: "03ee5be239c91c735d2c95db89a0d1b77e602cfc18af8eb64b3281fddcd90a72"
1394
1487
  },
1395
1488
  staleSpendRecoveryValidator: {
1396
- cid: "QmWqJZqoKUqMaGTXe9WUSwdDyeBwXJ9dnR3ioFnVK4RkN5",
1489
+ cid: "QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK",
1397
1490
  authorizedCidHex: cidToHex(
1398
- "QmWqJZqoKUqMaGTXe9WUSwdDyeBwXJ9dnR3ioFnVK4RkN5"
1491
+ "QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK"
1399
1492
  ),
1400
1493
  name: "Stale Spend Recovery Validator",
1401
1494
  description: "Production Stale Spend Recovery Validator",
1402
- version: "1.0.0",
1495
+ version: "1.1.2",
1403
1496
  deployed: true,
1404
- deployedAt: 1781542219438,
1405
- size: 34413,
1406
- hash: "26b11730cf5d9eb4af0b1fe65192d26ccee50369c0472211a0ea11e26ef51cc5",
1497
+ deployedAt: 1784316604502,
1498
+ size: 36785,
1499
+ hash: "28bfe44970541fefe3bae8bd54f25e8775d7cc61b5832a40ba1c18587b6b6358",
1407
1500
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1408
1501
  pkp: {
1409
1502
  publicKey: "0x046da0ad6da7dd4a0063cbeac6f2bdb6a9889c2178b23470833356a8ee5630f7f0eee0db348d9d9aaaadd23eccf0063c8f68ee5e446abe30b882bf1de4c932bc21",
@@ -2191,6 +2284,20 @@ ${errorReport}`);
2191
2284
  /* REGTEST */
2192
2285
  // Local development
2193
2286
  };
2287
+ var CHAIN_TO_CHAIN_ID = {
2288
+ [
2289
+ "ethereum"
2290
+ /* ETHEREUM */
2291
+ ]: 1,
2292
+ [
2293
+ "sepolia"
2294
+ /* SEPOLIA */
2295
+ ]: 11155111,
2296
+ [
2297
+ "hardhat"
2298
+ /* HARDHAT */
2299
+ ]: 1337
2300
+ };
2194
2301
  function validateChain(chain) {
2195
2302
  const normalized = chain.toLowerCase().trim();
2196
2303
  if (normalized === "sepolia") {
@@ -2487,6 +2594,27 @@ ${errorReport}`);
2487
2594
  function createProtocolParametersModule(config) {
2488
2595
  return new ProtocolParametersModule(config);
2489
2596
  }
2597
+ function parseVaultAddress(rawVaultAddress, chain) {
2598
+ try {
2599
+ const parsed = JSON.parse(rawVaultAddress);
2600
+ if (typeof parsed === "object" && parsed !== null) {
2601
+ const chainLower = chain.toLowerCase();
2602
+ if (chainLower === "sepolia") {
2603
+ return parsed.regtest || parsed.testnet || parsed.mainnet;
2604
+ }
2605
+ const bitcoinNetwork = getBitcoinNetworkForChain(chain);
2606
+ if (bitcoinNetwork === "regtest") {
2607
+ return parsed.regtest || parsed.testnet || parsed.mainnet;
2608
+ }
2609
+ if (bitcoinNetwork === "testnet") {
2610
+ return parsed.testnet || parsed.regtest || parsed.mainnet;
2611
+ }
2612
+ return parsed.mainnet;
2613
+ }
2614
+ } catch (e) {
2615
+ }
2616
+ return rawVaultAddress;
2617
+ }
2490
2618
  var VaultSnapshotModule = class {
2491
2619
  constructor(config) {
2492
2620
  this.config = config;
@@ -2594,112 +2722,11 @@ ${errorReport}`);
2594
2722
  timestamp: Date.now()
2595
2723
  };
2596
2724
  }
2597
- /**
2598
- * Fast vault snapshot using balance method only (no UTXO validation)
2599
- * This is optimized for speed and should be used when UTXO details are not needed.
2600
- *
2601
- * WARNING: This method skips UTXO validation. Use ONLY for non-critical operations.
2602
- * For authorization/security operations, ALWAYS use getVaultSnapshot().
2603
- *
2604
- * @param positionId - Position to snapshot
2605
- * @returns Complete VaultSnapshot with all metrics (using fast balance method)
2606
- */
2607
- async getVaultSnapshotFast(positionId) {
2608
- const positionState = await this.queryPositionState(positionId);
2609
- console.log(
2610
- `[Vault Snapshot] Raw vault address from contract: "${positionState.vaultAddress}"`
2611
- );
2612
- const useStubbedBtcData = this.config.debugOverrides?.useStubbedBtcData === true;
2613
- const [balanceResult, btcPriceUsd] = useStubbedBtcData ? this.buildStubbedBtcSnapshotInputs(positionState) : await Promise.all([
2614
- this.config.vaultBalance.calculateTrustedBalance(
2615
- positionId,
2616
- positionState.vaultAddress,
2617
- true
2618
- // Always skip UTXO validation for fast path
2619
- ),
2620
- this.config.priceOracle.getBTCPrice()
2621
- ]);
2622
- const protocolParams = createProtocolParametersModule({
2623
- termManagerAddress: this.config.termManagerAddress,
2624
- loanOpsManagerAddress: this.config.loanOpsManagerAddress,
2625
- liquidationManagerAddress: this.config.liquidationManagerAddress,
2626
- chain: this.config.chain,
2627
- chainId: this.config.chainId || 1,
2628
- // Default to mainnet if not provided
2629
- rpcUrl: this.config.rpcUrl
2630
- });
2631
- const termForFees = this.config.termOverride ?? positionState.selectedTerm;
2632
- const [liquidationThresholdBps, maxEscalatedThresholdBps, termFees] = await Promise.all([
2633
- protocolParams.getLiquidationThreshold(),
2634
- protocolParams.getMaxEscalatedThreshold(),
2635
- protocolParams.getTermFees(termForFees)
2636
- ]);
2637
- const termStatus = calculateTermStatus(
2638
- positionState.termStartTimestamp,
2639
- positionState.selectedTerm
2640
- );
2641
- const currentLiquidationThreshold = calculateLiquidationThreshold(
2642
- termStatus.isExpired,
2643
- termStatus.daysIntoGracePeriod,
2644
- liquidationThresholdBps,
2645
- maxEscalatedThresholdBps
2646
- );
2647
- const collateralMetrics = calculateCollateralMetrics(
2648
- balanceResult.availableBalance,
2649
- btcPriceUsd,
2650
- positionState.ucdDebt
2651
- );
2652
- const isLiquidatable = collateralMetrics.collateralRatioBps < currentLiquidationThreshold;
2653
- const marginToLiquidationBps = collateralMetrics.collateralRatioBps - currentLiquidationThreshold;
2654
- return {
2655
- // Raw state
2656
- positionId: positionState.positionId,
2657
- pkpId: positionState.pkpId,
2658
- borrower: positionState.borrower,
2659
- vaultAddress: positionState.vaultAddress,
2660
- ucdDebt: positionState.ucdDebt,
2661
- termStartTimestamp: positionState.termStartTimestamp,
2662
- selectedTerm: positionState.selectedTerm,
2663
- status: positionState.status,
2664
- // Loan status enum value (0-7)
2665
- expiryAt: positionState.expiryAt,
2666
- // Loan expiry timestamp
2667
- previousExpiryAt: positionState.previousExpiryAt,
2668
- // expiryAt before last renewal; 0 if never renewed
2669
- totalTerm: positionState.totalTerm,
2670
- // total accumulated loan months; 0 before first renewal
2671
- lastRenewedAt: positionState.lastRenewedAt,
2672
- // timestamp of most recent renewal; createdAt at origination; 0 for pre-H-5 positions
2673
- // Bitcoin state
2674
- totalBTCSats: balanceResult.totalBalance,
2675
- totalUTXOs: balanceResult.totalUTXOs,
2676
- authorizedSpendsSats: balanceResult.authorizedBalance,
2677
- authorizedSpendsHash: balanceResult.authorizedSpendsHash,
2678
- availableBTCSats: balanceResult.availableBalance,
2679
- availableUTXOs: balanceResult.availableUTXOs,
2680
- // Price data
2681
- btcPriceUsd,
2682
- // Calculated metrics
2683
- collateralValueUsd: collateralMetrics.collateralValueUsd,
2684
- collateralRatioBps: collateralMetrics.collateralRatioBps,
2685
- // Term status
2686
- termDurationDays: termStatus.termDurationDays,
2687
- termLengthDays: termStatus.termLengthDays,
2688
- isExpired: termStatus.isExpired,
2689
- daysUntilExpiry: termStatus.daysUntilExpiry,
2690
- daysIntoGracePeriod: termStatus.daysIntoGracePeriod,
2691
- // Liquidation risk
2692
- currentLiquidationThreshold,
2693
- isLiquidatable,
2694
- marginToLiquidationBps,
2695
- // Protocol parameters (from smart contracts)
2696
- liquidationThresholdBps,
2697
- originationFeeBps: termFees.originationFeeBps,
2698
- extensionFeeBps: termFees.extensionFeeBps,
2699
- // Metadata
2700
- timestamp: Date.now()
2701
- };
2702
- }
2725
+ // NOTE (remediation P1/#5): `getVaultSnapshotFast` — a snapshot built on the
2726
+ // skip-UTXO-validation balance path was deleted. It had zero callers, and
2727
+ // a snapshot whose balance skipped UTXO validation + consensus must never
2728
+ // exist within reach of a signing flow. Every snapshot now runs the full
2729
+ // validation path above.
2703
2730
  buildStubbedBtcSnapshotInputs(positionState) {
2704
2731
  const stubbedAvailableBtcSats = BigInt(
2705
2732
  this.config.debugOverrides?.stubbedAvailableBtcSats ?? "10000000"
@@ -2828,14 +2855,20 @@ ${errorReport}`);
2828
2855
  expiryAt,
2829
2856
  selectedTermMonths
2830
2857
  );
2858
+ const parsedVaultAddress = parseVaultAddress(
2859
+ position.vaultAddress,
2860
+ validateChain(this.config.chain)
2861
+ );
2862
+ if (typeof parsedVaultAddress !== "string" || parsedVaultAddress.length === 0) {
2863
+ throw new Error(
2864
+ `Position ${positionIdBytes32} vaultAddress has no entry for chain "${this.config.chain}"`
2865
+ );
2866
+ }
2831
2867
  return {
2832
2868
  positionId: position.positionId,
2833
2869
  pkpId: position.pkpId,
2834
2870
  borrower: position.borrower,
2835
- vaultAddress: this.parseVaultAddress(
2836
- position.vaultAddress,
2837
- validateChain(this.config.chain)
2838
- ),
2871
+ vaultAddress: parsedVaultAddress,
2839
2872
  ucdDebt: BigInt(position.ucdDebt.toString()),
2840
2873
  termStartTimestamp,
2841
2874
  selectedTerm: selectedTermMonths,
@@ -2846,36 +2879,8 @@ ${errorReport}`);
2846
2879
  lastRenewedAt
2847
2880
  };
2848
2881
  }
2849
- /**
2850
- * Parse vault address based on format and network
2851
- *
2852
- * Supports two formats:
2853
- * 1. Single string: "mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"
2854
- * 2. JSON object: {"mainnet":"bc1q...","testnet":"mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"}
2855
- *
2856
- * @param rawVaultAddress Raw vault address from contract
2857
- * @param chain EVM chain (sepolia, ethereum)
2858
- * @returns Parsed Bitcoin address for the appropriate network
2859
- */
2860
- parseVaultAddress(rawVaultAddress, chain) {
2861
- try {
2862
- const parsed = JSON.parse(rawVaultAddress);
2863
- if (typeof parsed === "object" && parsed !== null) {
2864
- const chainLower = chain.toLowerCase();
2865
- if (chainLower === "sepolia") {
2866
- return parsed.regtest || parsed.testnet || parsed.mainnet;
2867
- }
2868
- const bitcoinNetwork = getBitcoinNetworkForChain(chain);
2869
- if (bitcoinNetwork === "testnet") {
2870
- return parsed.testnet || parsed.regtest || parsed.mainnet;
2871
- } else {
2872
- return parsed.mainnet;
2873
- }
2874
- }
2875
- } catch (e) {
2876
- }
2877
- return rawVaultAddress;
2878
- }
2882
+ // parseVaultAddress moved to `modules/vault-address.ts` (shared with the
2883
+ // phase-2 signer for the P6/#7b vault-identity binding).
2879
2884
  /**
2880
2885
  * Quick check if position is liquidatable
2881
2886
  *
@@ -3213,6 +3218,13 @@ ${auth.clientId}`;
3213
3218
  return {};
3214
3219
  }
3215
3220
  }
3221
+ function assertSaneSatoshis(value, context) {
3222
+ if (!Number.isSafeInteger(value) || value < 0 || value > MAX_BTC_AMOUNT) {
3223
+ throw new Error(
3224
+ `${context}: satoshi value ${value} is not a sane amount (must be an integer in [0, ${MAX_BTC_AMOUNT}])`
3225
+ );
3226
+ }
3227
+ }
3216
3228
  var BitcoinDataProvider = class {
3217
3229
  constructor(config) {
3218
3230
  this.config = config;
@@ -3359,13 +3371,16 @@ ${auth.clientId}`;
3359
3371
  address
3360
3372
  );
3361
3373
  const utxos = unspentOutputs.map(
3362
- (utxo) => ({
3363
- txid: utxo.txid,
3364
- vout: utxo.vout,
3365
- satoshis: BigInt(Math.round(utxo.amount * 1e8)),
3366
- // BTC to satoshis as bigint
3367
- confirmations: utxo.confirmations
3368
- })
3374
+ (utxo) => {
3375
+ const satNumber = Math.round(utxo.amount * 1e8);
3376
+ assertSaneSatoshis(satNumber, `fetchUTXOsFromRPC ${utxo.txid}:${utxo.vout}`);
3377
+ return {
3378
+ txid: utxo.txid,
3379
+ vout: utxo.vout,
3380
+ satoshis: BigInt(satNumber),
3381
+ confirmations: utxo.confirmations
3382
+ };
3383
+ }
3369
3384
  );
3370
3385
  return utxos;
3371
3386
  }
@@ -3407,6 +3422,7 @@ ${auth.clientId}`;
3407
3422
  "Invalid UTXO row: missing txid, vout/n, or value/satoshis"
3408
3423
  );
3409
3424
  }
3425
+ assertSaneSatoshis(satRaw, `parseBlockstreamUTXOs ${txid}:${vout}`);
3410
3426
  utxos.push({
3411
3427
  txid,
3412
3428
  vout,
@@ -3585,7 +3601,10 @@ ${auth.clientId}`;
3585
3601
  `Invalid response: missing required fields. funded_txo_sum: ${fundedTxoSum}, spent_txo_sum: ${spentTxoSum}`
3586
3602
  );
3587
3603
  }
3604
+ assertSaneSatoshis(fundedTxoSum, `getBalance ${cleanAddress} funded_txo_sum`);
3605
+ assertSaneSatoshis(spentTxoSum, `getBalance ${cleanAddress} spent_txo_sum`);
3588
3606
  const confirmedBalance = fundedTxoSum - spentTxoSum;
3607
+ assertSaneSatoshis(confirmedBalance, `getBalance ${cleanAddress} confirmed balance`);
3589
3608
  const balance = BigInt(confirmedBalance);
3590
3609
  this.logger.log("3", `Balance: ${balance.toString()} sats`);
3591
3610
  this.logger.log(
@@ -4064,20 +4083,590 @@ ${auth.clientId}`;
4064
4083
  return priceWith8Decimals;
4065
4084
  }
4066
4085
  };
4086
+ var MAX_CLASSIFIED_PARENTS = 5;
4087
+ var CLASSIFICATION_CONCURRENCY = 2;
4088
+ var CONFIRMATION_SKEW_TOLERANCE = 3;
4089
+ async function mapWithConcurrency(items, limit, fn) {
4090
+ const results = new Array(items.length);
4091
+ let next = 0;
4092
+ const workers = Array.from(
4093
+ { length: Math.min(limit, items.length) },
4094
+ async () => {
4095
+ while (true) {
4096
+ const i = next++;
4097
+ if (i >= items.length) {
4098
+ return;
4099
+ }
4100
+ results[i] = await fn(items[i]);
4101
+ }
4102
+ }
4103
+ );
4104
+ await Promise.all(workers);
4105
+ return results;
4106
+ }
4107
+ function findOutputClaimMismatch(claimedUtxos, outputs, cleanVaultAddress) {
4108
+ for (const claim of claimedUtxos) {
4109
+ const output = outputs.find((o) => o.index === claim.vout);
4110
+ if (!output) {
4111
+ return `claimed output ${claim.txid}:${claim.vout} not present in parent tx`;
4112
+ }
4113
+ if (output.scriptpubkey_address !== cleanVaultAddress) {
4114
+ return `claimed output ${claim.txid}:${claim.vout} pays ${output.scriptpubkey_address ?? "<no address>"}, not the vault`;
4115
+ }
4116
+ if (output.value === void 0 || output.value !== claim.satoshis) {
4117
+ return `claimed output ${claim.txid}:${claim.vout} value ${output.value?.toString() ?? "<missing>"} != reported ${claim.satoshis.toString()}`;
4118
+ }
4119
+ }
4120
+ return null;
4121
+ }
4122
+ function stripNetworkPrefix(address) {
4123
+ const prefixPattern = /^(REGTEST_|TESTNET_|MAINNET_)/;
4124
+ return address.replace(prefixPattern, "");
4125
+ }
4126
+ var VaultBalanceConsensus = class {
4127
+ /**
4128
+ * @param minConsensusProviders Policy floor (`policy.minConsensusProviders`,
4129
+ * remediation P1/#1). Both consensus passes THROW when fewer providers are
4130
+ * configured than the floor demands — below it the cross-checks cannot run
4131
+ * and neither trusting the primary (inflation) nor excluding UTXOs
4132
+ * (deflation → wrongful liquidation) is a safe direction. A floor of 1
4133
+ * (hardhat / single-provider sepolia) preserves the trust-primary /
4134
+ * exclude-unconfirmed baseline unchanged.
4135
+ */
4136
+ constructor(primaryProvider, providerConfigs, minConsensusProviders) {
4137
+ this.primaryProvider = primaryProvider;
4138
+ this.providerConfigs = providerConfigs;
4139
+ this.minConsensusProviders = minConsensusProviders;
4140
+ this.readers = this.buildReaders();
4141
+ }
4142
+ /**
4143
+ * Fail-closed floor check shared by both consensus passes. Only floors above
4144
+ * 1 can bite: a floor of 1 with zero configs means "no esplora access
4145
+ * configured" (dev/test), where the documented pre-gate behaviour —
4146
+ * trust-primary confirmed set, exclude unconfirmed — remains correct.
4147
+ */
4148
+ assertProviderFloor(pass) {
4149
+ const configured = this.providerConfigs?.length ?? 0;
4150
+ if (this.minConsensusProviders > 1 && configured < this.minConsensusProviders) {
4151
+ throw new Error(
4152
+ `${pass}: this chain requires at least ${this.minConsensusProviders} Bitcoin providers for consensus; ${configured} configured. Refusing to proceed without cross-check.`
4153
+ );
4154
+ }
4155
+ }
4156
+ /**
4157
+ * Build one data reader per configured provider (primary first). Index 0
4158
+ * reuses the injected primary instance; the rest are constructed from their
4159
+ * configs so cross-check reads hit the right endpoint. Null when no providers
4160
+ * are configured — both passes then fall back to primary-only behaviour.
4161
+ */
4162
+ buildReaders() {
4163
+ const configs = this.providerConfigs;
4164
+ if (!configs || configs.length === 0) {
4165
+ return null;
4166
+ }
4167
+ return configs.map(
4168
+ (cfg, i) => i === 0 ? this.primaryProvider : new BitcoinDataProvider({
4169
+ providerUrl: cfg.providerUrl,
4170
+ auth: cfg.auth,
4171
+ ...cfg.timeout ? { timeout: cfg.timeout } : {}
4172
+ })
4173
+ );
4174
+ }
4175
+ /**
4176
+ * Confirmed-UTXO-set consensus across every registered provider (strict).
4177
+ *
4178
+ * The signed balance is dominated by the primary's confirmed UTXOs, but the
4179
+ * UTXO set — txid, vout, and value — is read only from the primary; the
4180
+ * `assertBitcoinBalanceConsensus` check the validators run compares
4181
+ * `chain_stats` totals, not individual outpoints. So a compromised primary
4182
+ * can add a fabricated confirmed UTXO, or inflate a real one's value, and the
4183
+ * balance consensus still passes (it serves honest `chain_stats`). This
4184
+ * closes that: each UTXO the primary reports at or above `minConfirmations`
4185
+ * must be present with the EXACT same value on every registered provider.
4186
+ *
4187
+ * Semantics:
4188
+ * - Absent on any provider, or value mismatch → THROW (fail-closed). Confirmed
4189
+ * transactions are fully propagated, so absence is unambiguous forgery, not
4190
+ * a race.
4191
+ * - Present + value-matched everywhere: the UTXO counts as confirmed only if
4192
+ * it meets `minConfirmations` on ALL providers (gated on the minimum
4193
+ * reported depth). A UTXO just past the boundary on the primary but a block
4194
+ * behind on a lagging provider is DEMOTED — excluded until it matures —
4195
+ * rather than signed over. A depth spread beyond CONFIRMATION_SKEW_TOLERANCE
4196
+ * is not benign skew but a provider lying about depth (to inflate a shallow
4197
+ * UTXO past the gate, or deflate a deep one into a wrongful liquidation) →
4198
+ * THROW.
4199
+ * - SYMMETRIC (finding #6): a confirmed outpoint any secondary reports that
4200
+ * the primary omits entirely, reports with a different value, or reports
4201
+ * at a depth more than the skew tolerance BELOW the secondary's → THROW.
4202
+ * Without this, a compromised primary deflates the balance by omission and
4203
+ * forces a wrongful liquidation.
4204
+ *
4205
+ * Below `minConsensusProviders` this THROWS (P1/#1). With a floor of 1 and
4206
+ * 0/1 providers configured, no cross-check is possible and the primary's
4207
+ * confirmed UTXOs are returned unchanged (behaviour identical to before
4208
+ * this gate).
4209
+ *
4210
+ * Cross-checking `chain_stats` totals against the confirmed-UTXO sum was
4211
+ * considered and rejected: `getBalance` reads mempool-blind `chain_stats`
4212
+ * while `getUTXOSet` removes mempool-spent outputs, so the two legitimately
4213
+ * diverge during any pending spend — precisely the self-change window — and
4214
+ * that comparison would false-fail there. Outpoint-set consensus asserts
4215
+ * agreement only on confirmed outpoints, which the mempool does not touch.
4216
+ *
4217
+ * @returns Set of "txid:vout" keys admitted as confirmed-with-consensus.
4218
+ */
4219
+ async confirmedUtxoConsensus(primaryUtxos, vaultAddress, minConfirmations) {
4220
+ const providerConfigs = this.providerConfigs;
4221
+ const readers = this.readers;
4222
+ this.assertProviderFloor("confirmedUtxoConsensus");
4223
+ const confirmedKeys = /* @__PURE__ */ new Set();
4224
+ const primaryConfirmed = primaryUtxos.filter(
4225
+ (utxo) => utxo.confirmations >= minConfirmations
4226
+ );
4227
+ for (const utxo of primaryConfirmed) {
4228
+ confirmedKeys.add(`${utxo.txid}:${utxo.vout}`);
4229
+ }
4230
+ if (!providerConfigs || providerConfigs.length <= 1 || !readers) {
4231
+ return confirmedKeys;
4232
+ }
4233
+ const providerName = (i) => providerConfigs[i].name || providerConfigs[i].providerUrl;
4234
+ const settled = await Promise.allSettled(
4235
+ readers.slice(1).map((reader) => reader.getUTXOSet(vaultAddress, minConfirmations))
4236
+ );
4237
+ const failures = settled.map((r, i) => ({ r, name: providerName(i + 1) })).filter(
4238
+ (x) => x.r.status === "rejected"
4239
+ ).map(
4240
+ ({ r, name }) => `${name} \u2192 ${r.reason?.message ?? String(r.reason)}`
4241
+ );
4242
+ if (failures.length > 0) {
4243
+ throw new Error(
4244
+ `Confirmed UTXO set read failed for ${vaultAddress} on ${failures.length}/${providerConfigs.length - 1} secondary provider(s): ${failures.join("; ")}`
4245
+ );
4246
+ }
4247
+ const secondaryMaps = settled.map((r) => {
4248
+ const map2 = /* @__PURE__ */ new Map();
4249
+ for (const utxo of r.value.utxos) {
4250
+ map2.set(`${utxo.txid}:${utxo.vout}`, {
4251
+ value: utxo.satoshis,
4252
+ confirmations: utxo.confirmations
4253
+ });
4254
+ }
4255
+ return map2;
4256
+ });
4257
+ const demoted = [];
4258
+ for (const utxo of primaryConfirmed) {
4259
+ const key = `${utxo.txid}:${utxo.vout}`;
4260
+ let minConfs = utxo.confirmations;
4261
+ let maxConfs = utxo.confirmations;
4262
+ for (let s = 0; s < secondaryMaps.length; s++) {
4263
+ const entry = secondaryMaps[s].get(key);
4264
+ const name = providerName(s + 1);
4265
+ if (!entry) {
4266
+ throw new Error(
4267
+ `Confirmed UTXO ${key} present on ${providerName(0)} but absent on ${name}; refusing to sign (possible fabricated UTXO).`
4268
+ );
4269
+ }
4270
+ if (entry.value !== utxo.satoshis) {
4271
+ throw new Error(
4272
+ `Confirmed UTXO ${key} value mismatch: ${providerName(0)}=${utxo.satoshis.toString()} vs ${name}=${entry.value.toString()}; refusing to sign.`
4273
+ );
4274
+ }
4275
+ if (entry.confirmations < minConfs)
4276
+ minConfs = entry.confirmations;
4277
+ if (entry.confirmations > maxConfs)
4278
+ maxConfs = entry.confirmations;
4279
+ }
4280
+ if (maxConfs - minConfs > CONFIRMATION_SKEW_TOLERANCE) {
4281
+ throw new Error(
4282
+ `Confirmed UTXO ${key} confirmation spread ${maxConfs - minConfs} exceeds tolerance ${CONFIRMATION_SKEW_TOLERANCE} (min ${minConfs}, max ${maxConfs}); providers disagree on depth, refusing to sign.`
4283
+ );
4284
+ }
4285
+ if (minConfs < minConfirmations) {
4286
+ confirmedKeys.delete(key);
4287
+ demoted.push(key);
4288
+ }
4289
+ }
4290
+ if (demoted.length > 0) {
4291
+ litDebugLog(
4292
+ `[VaultBalance] confirmed-consensus: ${demoted.length} UTXO(s) demoted pending provider tip skew (excluded until matured): ${demoted.join(", ")}`
4293
+ );
4294
+ }
4295
+ const primaryByKey = new Map(
4296
+ primaryUtxos.map((utxo) => [`${utxo.txid}:${utxo.vout}`, utxo])
4297
+ );
4298
+ for (let s = 0; s < secondaryMaps.length; s++) {
4299
+ const name = providerName(s + 1);
4300
+ for (const [key, entry] of secondaryMaps[s]) {
4301
+ if (entry.confirmations < minConfirmations)
4302
+ continue;
4303
+ const primaryUtxo = primaryByKey.get(key);
4304
+ if (!primaryUtxo) {
4305
+ throw new Error(
4306
+ `Confirmed UTXO ${key} present on ${name} (${entry.confirmations} confs) but absent on ${providerName(0)}; refusing to sign (possible omitted UTXO / balance deflation \u2014 may be transient during an in-flight vault spend).`
4307
+ );
4308
+ }
4309
+ if (primaryUtxo.satoshis !== entry.value) {
4310
+ throw new Error(
4311
+ `Confirmed UTXO ${key} value mismatch: ${providerName(0)}=${primaryUtxo.satoshis.toString()} vs ${name}=${entry.value.toString()}; refusing to sign.`
4312
+ );
4313
+ }
4314
+ if (entry.confirmations - primaryUtxo.confirmations > CONFIRMATION_SKEW_TOLERANCE) {
4315
+ throw new Error(
4316
+ `Confirmed UTXO ${key} depth disagreement: ${providerName(0)}=${primaryUtxo.confirmations} vs ${name}=${entry.confirmations} exceeds tolerance ${CONFIRMATION_SKEW_TOLERANCE}; the primary may be under-reporting depth to suppress collateral, refusing to sign.`
4317
+ );
4318
+ }
4319
+ }
4320
+ }
4321
+ return confirmedKeys;
4322
+ }
4323
+ /**
4324
+ * Classify unconfirmed UTXOs as protocol self-change.
4325
+ *
4326
+ * A UTXO below minConfirmations counts as confirmed-equivalent ONLY when the
4327
+ * transaction that created it spends exclusively vault-owned, confirmed
4328
+ * outputs. Such a transaction can only have been signed by the vault's PKP,
4329
+ * so no third party can RBF-replace or double-spend it — the reversal risk
4330
+ * the confirmation gate exists to close does not apply. Any non-vault input,
4331
+ * unconfirmed funding tx, or coinbase input disqualifies the UTXO
4332
+ * (fail-safe: exclusion, never inclusion).
4333
+ *
4334
+ * Multi-provider consensus (audit C-1/C-2 parity): the parent-tx proof is
4335
+ * verified independently against EVERY configured provider and admitted
4336
+ * only on unanimity. The confirmed-balance consensus the validators run
4337
+ * covers `chain_stats` only, so without this the primary provider alone
4338
+ * could fabricate an unconfirmed UTXO plus a self-consistent parent proof
4339
+ * and inflate the signed balance. Any provider failure or verdict
4340
+ * disagreement THROWS (fail-closed) — exclusion on a suppressed proof
4341
+ * would deflate the balance and open a wrongful-liquidation window, so
4342
+ * neither direction may proceed without agreement.
4343
+ *
4344
+ * The same per-provider `/tx` fetch also returns the parent's outputs, and
4345
+ * every provider must corroborate each claimed UTXO (index, vault address,
4346
+ * exact value) before admission — the UTXO list itself comes only from the
4347
+ * primary, so an unchecked claim would let it attach a fabricated or
4348
+ * inflated output to a genuine self-spend.
4349
+ *
4350
+ * Fetch cost: zero when every UTXO is confirmed (the common case); otherwise
4351
+ * per provider: one tx lookup per classified parent (vins + vouts in one
4352
+ * response), plus per distinct funding tx a confirmation check and — only
4353
+ * when the provider omits vin prevout data — an output lookup. Classified
4354
+ * parents are capped at MAX_CLASSIFIED_PARENTS (largest claimed value
4355
+ * first, deterministic) and evaluated CLASSIFICATION_CONCURRENCY at a time,
4356
+ * so third-party dust spam cannot amplify into provider rate-limiting and
4357
+ * an action-wide outage.
4358
+ *
4359
+ * See docs/vault-balance-confirmed-gate-plan-2026-07-14.md §3.1.
4360
+ *
4361
+ * @param utxos - Full UTXO set for the vault address
4362
+ * @param vaultAddress - Bitcoin address of the vault (may carry network prefix)
4363
+ * @param minConfirmations - Confirmation threshold for the chain
4364
+ * @returns Set of "txid:vout" keys for UTXOs verified as self-change
4365
+ */
4366
+ async classifySelfChange(utxos, vaultAddress, minConfirmations) {
4367
+ const providerConfigs = this.providerConfigs;
4368
+ const readers = this.readers;
4369
+ const selfChange = /* @__PURE__ */ new Set();
4370
+ const unconfirmed = utxos.filter(
4371
+ (utxo) => utxo.confirmations < minConfirmations
4372
+ );
4373
+ if (unconfirmed.length === 0) {
4374
+ return selfChange;
4375
+ }
4376
+ this.assertProviderFloor("classifySelfChange");
4377
+ if (!providerConfigs || providerConfigs.length === 0 || !readers) {
4378
+ litDebugLog(
4379
+ `[VaultBalance] ${unconfirmed.length} unconfirmed UTXO(s) excluded: no bitcoinProviderConfigs for self-change classification`
4380
+ );
4381
+ return selfChange;
4382
+ }
4383
+ const cleanVaultAddress = stripNetworkPrefix(vaultAddress);
4384
+ const claimsByParent = /* @__PURE__ */ new Map();
4385
+ for (const utxo of unconfirmed) {
4386
+ const claims = claimsByParent.get(utxo.txid) ?? [];
4387
+ claims.push(utxo);
4388
+ claimsByParent.set(utxo.txid, claims);
4389
+ }
4390
+ const claimedSum = (txid) => claimsByParent.get(txid).reduce((sum, utxo) => sum + utxo.satoshis, 0n);
4391
+ const rankedParents = [...claimsByParent.keys()].sort((a, b) => {
4392
+ const diff = claimedSum(b) - claimedSum(a);
4393
+ if (diff !== 0n) {
4394
+ return diff > 0n ? 1 : -1;
4395
+ }
4396
+ return a < b ? -1 : a > b ? 1 : 0;
4397
+ });
4398
+ const parentTxids = rankedParents.slice(0, MAX_CLASSIFIED_PARENTS);
4399
+ const skippedParents = rankedParents.length - parentTxids.length;
4400
+ if (skippedParents > 0) {
4401
+ litDebugLog(
4402
+ `[VaultBalance] self-change: ${skippedParents} parent tx(s) beyond the ${MAX_CLASSIFIED_PARENTS}-parent classification budget excluded (mempool-spam bound; largest claimed value classified first)`
4403
+ );
4404
+ }
4405
+ const verdicts = await mapWithConcurrency(
4406
+ parentTxids,
4407
+ CLASSIFICATION_CONCURRENCY,
4408
+ async (parentTxid) => ({
4409
+ parentTxid,
4410
+ verified: await this.verifyParentIsSelfSpendConsensus(
4411
+ parentTxid,
4412
+ claimsByParent.get(parentTxid),
4413
+ cleanVaultAddress,
4414
+ minConfirmations,
4415
+ providerConfigs,
4416
+ readers
4417
+ )
4418
+ })
4419
+ );
4420
+ const verifiedParents = new Set(
4421
+ verdicts.filter((v) => v.verified).map((v) => v.parentTxid)
4422
+ );
4423
+ for (const utxo of unconfirmed) {
4424
+ if (verifiedParents.has(utxo.txid)) {
4425
+ selfChange.add(`${utxo.txid}:${utxo.vout}`);
4426
+ }
4427
+ }
4428
+ if (verifiedParents.size > 0) {
4429
+ litDebugLog(
4430
+ `[VaultBalance] self-change: ${selfChange.size}/${unconfirmed.length} unconfirmed UTXO(s) verified as protocol change by ${providerConfigs.length} provider(s)`
4431
+ );
4432
+ }
4433
+ return selfChange;
4434
+ }
4435
+ /**
4436
+ * Run the self-spend proof against every configured provider and require
4437
+ * unanimity. Fail-closed: a provider request failure or a verdict
4438
+ * disagreement throws and aborts the balance calculation — see
4439
+ * classifySelfChange for why exclusion is not a safe fallback here.
4440
+ *
4441
+ * When the parent IS a unanimous self-spend, every provider's view of the
4442
+ * parent's OUTPUTS must additionally corroborate the primary's claimed
4443
+ * UTXOs (index exists, pays the vault, exact value). Without this, a
4444
+ * compromised primary could attach a fabricated or value-inflated output
4445
+ * to a genuine self-spend the honest providers happily verify.
4446
+ *
4447
+ * @returns True when every provider verified the parent and corroborated
4448
+ * the claimed outputs, false when every provider rejected the
4449
+ * parent (genuine third-party deposit)
4450
+ */
4451
+ async verifyParentIsSelfSpendConsensus(parentTxid, claimedUtxos, cleanVaultAddress, minConfirmations, providerConfigs, readers) {
4452
+ const providerName = (i) => providerConfigs[i].name || providerConfigs[i].providerUrl;
4453
+ const settled = await Promise.allSettled(
4454
+ providerConfigs.map(
4455
+ (cfg, i) => this.evaluateParentForProvider(
4456
+ parentTxid,
4457
+ claimedUtxos,
4458
+ cleanVaultAddress,
4459
+ minConfirmations,
4460
+ readers[i],
4461
+ cfg
4462
+ )
4463
+ )
4464
+ );
4465
+ const failures = settled.map((r, i) => ({ r, name: providerName(i) })).filter(
4466
+ (x) => x.r.status === "rejected"
4467
+ ).map(
4468
+ ({ r, name }) => `${name} \u2192 ${r.reason?.message ?? String(r.reason)}`
4469
+ );
4470
+ if (failures.length > 0) {
4471
+ throw new Error(
4472
+ `Self-change classification failed for tx ${parentTxid} on ${failures.length}/${providerConfigs.length} provider(s): ${failures.join("; ")}`
4473
+ );
4474
+ }
4475
+ const evaluations = settled.map(
4476
+ (r) => r.value
4477
+ );
4478
+ const votes = evaluations.map((e) => e.selfSpend);
4479
+ const disagreements = votes.map((vote, i) => ({ vote, name: providerName(i) })).filter((x) => x.vote !== votes[0]).map(
4480
+ (x) => `${providerName(0)}=${votes[0]} vs ${x.name}=${x.vote}`
4481
+ );
4482
+ if (disagreements.length > 0) {
4483
+ throw new Error(
4484
+ `Self-change consensus mismatch for tx ${parentTxid}: ${disagreements.join("; ")}. A provider may be compromised or out of sync; refusing to sign.`
4485
+ );
4486
+ }
4487
+ if (!votes[0]) {
4488
+ return false;
4489
+ }
4490
+ const outputMismatches = evaluations.map((e, i) => e.outputMismatch ? `${providerName(i)} \u2192 ${e.outputMismatch}` : null).filter((m) => m !== null);
4491
+ if (outputMismatches.length > 0) {
4492
+ throw new Error(
4493
+ `Self-change output claim mismatch for tx ${parentTxid}: ${outputMismatches.join("; ")}. The reported UTXO set contradicts the parent transaction; refusing to sign.`
4494
+ );
4495
+ }
4496
+ return true;
4497
+ }
4498
+ /**
4499
+ * Single provider's evaluation of a parent tx: the self-spend proof plus —
4500
+ * when the proof holds — corroboration of the claimed outputs against this
4501
+ * provider's view of the parent's vout set. The output check is skipped for
4502
+ * non-self-spend parents (their UTXOs are excluded regardless, and a
4503
+ * third-party deposit's values are not ours to police).
4504
+ */
4505
+ async evaluateParentForProvider(parentTxid, claimedUtxos, cleanVaultAddress, minConfirmations, reader, providerConfig) {
4506
+ const parentTx = await this.fetchParentTransaction(
4507
+ parentTxid,
4508
+ providerConfig
4509
+ );
4510
+ if (!parentTx || parentTx.inputs.length === 0) {
4511
+ return { selfSpend: false, outputMismatch: null };
4512
+ }
4513
+ const selfSpend = await this.verifyInputsAreSelfSpend(
4514
+ parentTx.inputs,
4515
+ cleanVaultAddress,
4516
+ minConfirmations,
4517
+ reader
4518
+ );
4519
+ const outputMismatch = selfSpend ? findOutputClaimMismatch(claimedUtxos, parentTx.outputs, cleanVaultAddress) : null;
4520
+ return { selfSpend, outputMismatch };
4521
+ }
4522
+ /**
4523
+ * Verify that a transaction's inputs spend exclusively vault-owned,
4524
+ * confirmed outputs, using a single provider's view of the chain.
4525
+ *
4526
+ * @param inputs - Parsed inputs of the parent transaction
4527
+ * @param cleanVaultAddress - Vault address with network prefix stripped
4528
+ * @param minConfirmations - Required depth for every funding transaction
4529
+ * @param reader - Data provider for funding-tx lookups (same provider the inputs came from)
4530
+ * @returns True only when every input is a confirmed, vault-owned outpoint
4531
+ */
4532
+ async verifyInputsAreSelfSpend(inputs, cleanVaultAddress, minConfirmations, reader) {
4533
+ const fundingTxids = [...new Set(inputs.map((input) => input.txid))];
4534
+ const outputsByFundingTx = /* @__PURE__ */ new Map();
4535
+ const needsOutputLookup = fundingTxids.filter(
4536
+ (fundingTxid) => inputs.some(
4537
+ (input) => input.txid === fundingTxid && input.prevoutAddress === void 0
4538
+ )
4539
+ );
4540
+ await Promise.all(
4541
+ needsOutputLookup.map(async (fundingTxid) => {
4542
+ outputsByFundingTx.set(
4543
+ fundingTxid,
4544
+ await reader.getTransactionOutputs(fundingTxid)
4545
+ );
4546
+ })
4547
+ );
4548
+ for (const input of inputs) {
4549
+ let ownerAddress = input.prevoutAddress;
4550
+ if (ownerAddress === void 0) {
4551
+ const outputs = outputsByFundingTx.get(input.txid);
4552
+ ownerAddress = outputs?.find(
4553
+ (output) => output.index === input.vout
4554
+ )?.scriptpubkey_address;
4555
+ }
4556
+ if (ownerAddress !== cleanVaultAddress) {
4557
+ return false;
4558
+ }
4559
+ }
4560
+ const fundingStatuses = await Promise.all(
4561
+ fundingTxids.map((fundingTxid) => reader.getTransaction(fundingTxid))
4562
+ );
4563
+ for (const status of fundingStatuses) {
4564
+ if (!status || status.confirmations < minConfirmations) {
4565
+ return false;
4566
+ }
4567
+ }
4568
+ return true;
4569
+ }
4570
+ /**
4571
+ * Fetch a parent transaction's inputs (vins) and outputs (vouts) via Esplora.
4572
+ *
4573
+ * A module-private fetch (not a `BitcoinDataProvider` method) so editing this
4574
+ * consensus logic does not rebundle the shared provider module's other
4575
+ * importers — see this file's header for the ripple-set constraint.
4576
+ *
4577
+ * @param txid - Transaction ID
4578
+ * @param providerConfig - Esplora endpoint to query (one of the configured
4579
+ * consensus providers)
4580
+ * @returns Parsed inputs + outputs, or null ONLY when the tx is provably not
4581
+ * a self-spend candidate (coinbase-funded). Provider inability —
4582
+ * 404, malformed vin rows, missing vin array — THROWS (review
4583
+ * 2026-07-15 P1): the parent of a UTXO the primary lists must
4584
+ * exist, so "can't fetch it" is a provider failure, and treating it
4585
+ * as a false self-change vote would silently deflate collateral
4586
+ * (wrongful-liquidation direction) if every provider failed the
4587
+ * same way.
4588
+ */
4589
+ async fetchParentTransaction(txid, providerConfig) {
4590
+ if (!providerConfig.providerUrl) {
4591
+ return null;
4592
+ }
4593
+ const timeout = providerConfig.timeout || 15e3;
4594
+ const controller = new AbortController();
4595
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
4596
+ try {
4597
+ const response = await fetch(`${providerConfig.providerUrl}/tx/${txid}`, {
4598
+ signal: controller.signal,
4599
+ headers: await authHeaders(providerConfig.auth, timeout)
4600
+ });
4601
+ if (!response.ok) {
4602
+ if (response.status === 404) {
4603
+ throw new Error(
4604
+ `parent tx ${txid} not found on this provider \u2014 cannot verify the self-change proof`
4605
+ );
4606
+ }
4607
+ throw new Error(
4608
+ `Bitcoin provider error: ${response.status} ${response.statusText}`
4609
+ );
4610
+ }
4611
+ const data = await response.json();
4612
+ if (!data.vin || !Array.isArray(data.vin) || data.vin.length === 0) {
4613
+ throw new Error(
4614
+ `provider returned tx ${txid} without a vin array \u2014 malformed response`
4615
+ );
4616
+ }
4617
+ const inputs = [];
4618
+ for (const vin of data.vin) {
4619
+ if (vin.is_coinbase) {
4620
+ return null;
4621
+ }
4622
+ if (vin.txid === void 0 || vin.vout === void 0) {
4623
+ throw new Error(
4624
+ `provider returned tx ${txid} with a malformed vin row (missing txid/vout)`
4625
+ );
4626
+ }
4627
+ inputs.push({
4628
+ txid: vin.txid,
4629
+ vout: vin.vout,
4630
+ prevoutAddress: vin.prevout?.scriptpubkey_address
4631
+ });
4632
+ }
4633
+ const outputs = Array.isArray(data.vout) ? data.vout.map((vout, index) => ({
4634
+ index,
4635
+ scriptpubkey_address: vout?.scriptpubkey_address,
4636
+ value: typeof vout?.value === "number" ? BigInt(vout.value) : void 0
4637
+ })) : [];
4638
+ return { inputs, outputs };
4639
+ } catch (error) {
4640
+ if (isAbortError(error)) {
4641
+ throw new Error(`Request timeout after ${timeout}ms`);
4642
+ }
4643
+ throw error;
4644
+ } finally {
4645
+ clearTimeout(timeoutId);
4646
+ }
4647
+ }
4648
+ };
4067
4649
  var VaultBalanceModule = class {
4068
4650
  constructor(config) {
4651
+ if (!Number.isInteger(config.minConfirmations) || config.minConfirmations < 1) {
4652
+ throw new Error(
4653
+ `VaultBalanceModule: minConfirmations must be a positive integer (got ${config.minConfirmations}). Pass the policy-derived balance-confirmation threshold \u2014 there is no default.`
4654
+ );
4655
+ }
4069
4656
  this.config = config;
4070
4657
  this.bitcoinProvider = config.bitcoinProvider;
4071
4658
  }
4072
4659
  /**
4073
- * Calculate balance (fast path - no UTXO validation)
4660
+ * Calculate balance (arithmetic only - no UTXO validation)
4074
4661
  *
4075
- * Fast balance-only calculation that skips UTXO validation.
4076
- * Uses simple arithmetic: Total Balance - Authorized Balance = Available Balance
4662
+ * Balance-only calculation: Total Balance - Authorized Balance = Available.
4077
4663
  *
4078
- * Use this when you only need the balance amount and don't need:
4079
- * - Individual UTXO details
4080
- * - Validation that authorized UTXOs were actually spent
4664
+ * ⚠️ SECURITY (remediation P1/#5): this result skips UTXO validation, the
4665
+ * confirmed-consensus gate, and self-change proofing. It must NEVER feed a
4666
+ * signed BalanceConfirmation or any authorization decision. No production
4667
+ * action calls it (the `skipUTXOValidation` fast path that used to route
4668
+ * here was deleted); it exists for read-only diagnostics and tests. Adding
4669
+ * a production caller requires Master sign-off.
4081
4670
  *
4082
4671
  * @param positionId - The position ID to check
4083
4672
  * @param vaultAddress - Bitcoin address of the vault
@@ -4124,17 +4713,18 @@ ${auth.clientId}`;
4124
4713
  * 1. Authorized transaction not found on network (not broadcasted)
4125
4714
  * 2. Authorized transaction has insufficient confirmations (network updating)
4126
4715
  *
4716
+ * NOTE: the `skipUTXOValidation` fast path was removed (remediation P1/#5).
4717
+ * Every trusted-balance read runs full UTXO validation + consensus; a
4718
+ * balance that skipped validation must never feed a signed
4719
+ * BalanceConfirmation.
4720
+ *
4127
4721
  * @param positionId - The position ID to check
4128
4722
  * @param vaultAddress - Bitcoin address of the vault
4129
- * @param skipUTXOValidation - If true, use fast balance-only path (skips UTXO validation)
4130
4723
  * @returns Complete breakdown of vault balance
4131
4724
  * @throws Error if authorized spends are in invalid state
4132
4725
  */
4133
- async calculateTrustedBalance(positionId, vaultAddress, skipUTXOValidation) {
4134
- if (skipUTXOValidation) {
4135
- return await this.calculateBalance(positionId, vaultAddress);
4136
- }
4137
- const minConfirmations = this.config.minConfirmations || 6;
4726
+ async calculateTrustedBalance(positionId, vaultAddress) {
4727
+ const minConfirmations = this.config.minConfirmations;
4138
4728
  const utxoSet = await this.bitcoinProvider.getUTXOSet(
4139
4729
  vaultAddress,
4140
4730
  minConfirmations
@@ -4154,8 +4744,25 @@ ${auth.clientId}`;
4154
4744
  (sum, spend) => sum + spend.satoshis,
4155
4745
  0n
4156
4746
  );
4747
+ const consensus = new VaultBalanceConsensus(
4748
+ this.bitcoinProvider,
4749
+ this.config.bitcoinProviderConfigs,
4750
+ // Absent ⇒ 1: dev/test constructions without a policy keep the
4751
+ // single-provider baseline; validators pass policy.minConsensusProviders.
4752
+ this.config.minConsensusProviders ?? 1
4753
+ );
4754
+ const confirmedConsensusKeys = await consensus.confirmedUtxoConsensus(
4755
+ utxoSet.utxos,
4756
+ vaultAddress,
4757
+ minConfirmations
4758
+ );
4759
+ const selfChangeKeys = await consensus.classifySelfChange(
4760
+ utxoSet.utxos,
4761
+ vaultAddress,
4762
+ minConfirmations
4763
+ );
4157
4764
  const availableUTXOs = utxoSet.utxos.filter(
4158
- (utxo) => !this.isUTXOAuthorized(utxo, authorizedSpends)
4765
+ (utxo) => (confirmedConsensusKeys.has(`${utxo.txid}:${utxo.vout}`) || selfChangeKeys.has(`${utxo.txid}:${utxo.vout}`)) && !this.isUTXOAuthorized(utxo, authorizedSpends)
4159
4766
  );
4160
4767
  const availableBalance = availableUTXOs.reduce(
4161
4768
  (sum, utxo) => sum + utxo.satoshis,
@@ -4387,6 +4994,76 @@ ${auth.clientId}`;
4387
4994
  function createVaultBalanceModule(config) {
4388
4995
  return new VaultBalanceModule(config);
4389
4996
  }
4997
+ var POLICY_BY_CHAIN = {
4998
+ [
4999
+ "hardhat"
5000
+ /* HARDHAT */
5001
+ ]: {
5002
+ chain: "hardhat",
5003
+ chainId: CHAIN_TO_CHAIN_ID[
5004
+ "hardhat"
5005
+ /* HARDHAT */
5006
+ ],
5007
+ bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
5008
+ "hardhat"
5009
+ /* HARDHAT */
5010
+ ],
5011
+ allowArbitraryBitcoinProvider: true,
5012
+ allowArbitraryEvmRpc: true,
5013
+ allowDevSinglePriceProvider: true,
5014
+ minBitcoinBalanceConfirmations: 1,
5015
+ minBitcoinSpendConfirmations: 1,
5016
+ minConsensusProviders: 1
5017
+ },
5018
+ [
5019
+ "sepolia"
5020
+ /* SEPOLIA */
5021
+ ]: {
5022
+ chain: "sepolia",
5023
+ chainId: CHAIN_TO_CHAIN_ID[
5024
+ "sepolia"
5025
+ /* SEPOLIA */
5026
+ ],
5027
+ bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
5028
+ "sepolia"
5029
+ /* SEPOLIA */
5030
+ ],
5031
+ allowArbitraryBitcoinProvider: false,
5032
+ allowArbitraryEvmRpc: false,
5033
+ allowDevSinglePriceProvider: false,
5034
+ minBitcoinBalanceConfirmations: 1,
5035
+ minBitcoinSpendConfirmations: 1,
5036
+ minConsensusProviders: 1
5037
+ },
5038
+ [
5039
+ "ethereum"
5040
+ /* ETHEREUM */
5041
+ ]: {
5042
+ chain: "ethereum",
5043
+ chainId: CHAIN_TO_CHAIN_ID[
5044
+ "ethereum"
5045
+ /* ETHEREUM */
5046
+ ],
5047
+ bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
5048
+ "ethereum"
5049
+ /* ETHEREUM */
5050
+ ],
5051
+ allowArbitraryBitcoinProvider: false,
5052
+ allowArbitraryEvmRpc: false,
5053
+ allowDevSinglePriceProvider: false,
5054
+ minBitcoinBalanceConfirmations: 6,
5055
+ minBitcoinSpendConfirmations: 6,
5056
+ minConsensusProviders: 2
5057
+ }
5058
+ };
5059
+ function getChainPolicy(chain) {
5060
+ const validated = validateChain(chain);
5061
+ const policy = POLICY_BY_CHAIN[validated];
5062
+ if (!policy) {
5063
+ throw new Error(`No chain policy registered for "${chain}"`);
5064
+ }
5065
+ return policy;
5066
+ }
4390
5067
  async function executeVaultSnapshot(params) {
4391
5068
  global.ethers = {
4392
5069
  ...import_ethers16.ethers,
@@ -4431,7 +5108,7 @@ ${auth.clientId}`;
4431
5108
  // Default to mainnet if not provided
4432
5109
  rpcUrl: params.rpcUrl,
4433
5110
  bitcoinProvider,
4434
- minConfirmations: params.minConfirmations || 6
5111
+ minConfirmations: params.minConfirmations ?? getChainPolicy(params.chain).minBitcoinBalanceConfirmations
4435
5112
  });
4436
5113
  const vaultSnapshot = createVaultSnapshotModule({
4437
5114
  contractAddress: params.contractAddress,
@@ -4665,7 +5342,8 @@ function getSepoliaConfig() {
4665
5342
  contractVersionRegistry: SEPOLIA_CONTRACTS.ContractVersionRegistry || "",
4666
5343
  feeRecipientRegistry: SEPOLIA_CONTRACTS.FeeRecipientRegistry || "",
4667
5344
  positionDelegateRegistry: SEPOLIA_CONTRACTS.PositionDelegateRegistry || "",
4668
- bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
5345
+ bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
5346
+ carryAgentRegistry: SEPOLIA_CONTRACTS.CarryAgentRegistry || ""
4669
5347
  },
4670
5348
  subgraphs: {
4671
5349
  // Lazy getter for the same module-init snapshot reason as serviceEndpoint.
@@ -4705,7 +5383,8 @@ function getMainnetConfig() {
4705
5383
  contractVersionRegistry: MAINNET_CONTRACTS.ContractVersionRegistry || "",
4706
5384
  feeRecipientRegistry: MAINNET_CONTRACTS.FeeRecipientRegistry || "",
4707
5385
  positionDelegateRegistry: MAINNET_CONTRACTS.PositionDelegateRegistry || "",
4708
- bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
5386
+ bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
5387
+ carryAgentRegistry: MAINNET_CONTRACTS.CarryAgentRegistry || ""
4709
5388
  },
4710
5389
  subgraphs: {
4711
5390
  diamondHandsUrl: "https://gateway-arbitrum.network.thegraph.com/api/subgraphs/id/CCTPsdYqco2jChDLLBQTbdJWwoukVoMt1cXeR9ti6r9A"
@@ -5824,26 +6503,144 @@ async function buildSignedLoginPayload(signer, chainId, audience) {
5824
6503
  return { chainId, message, signature };
5825
6504
  }
5826
6505
 
6506
+ // src/utils/server-session-store.ts
6507
+ var REAUTH_WINDOW_SECONDS = 24 * 60 * 60;
6508
+ var SAFETY_MARGIN_SECONDS = 5 * 60;
6509
+ var STORE_PREFIX = "dh-server-session:";
6510
+ function sessionStoreKey(address, chainId, endpoint) {
6511
+ return STORE_PREFIX + `${address.trim().toLowerCase()}:${chainId}:${endpoint.replace(/\/+$/, "")}`;
6512
+ }
6513
+ function isPayloadShape(value) {
6514
+ return !!value && typeof value === "object" && typeof value.chainId === "number" && typeof value.signature === "string" && !!value.message && typeof value.message.address === "string" && typeof value.message.issuedAt === "number" && typeof value.message.nonce === "string";
6515
+ }
6516
+ function isPersistedShape(value) {
6517
+ return !!value && typeof value === "object" && value.version === 1 && typeof value.address === "string" && typeof value.chainId === "number" && typeof value.endpoint === "string" && isPayloadShape(value.payload) && (value.token === void 0 || typeof value.token === "string") && (value.tokenExpiresAt === void 0 || typeof value.tokenExpiresAt === "number");
6518
+ }
6519
+ var LocalStorageSessionStore = class {
6520
+ load(key) {
6521
+ let raw;
6522
+ try {
6523
+ raw = window.localStorage.getItem(key);
6524
+ } catch {
6525
+ return null;
6526
+ }
6527
+ if (!raw)
6528
+ return null;
6529
+ let parsed;
6530
+ try {
6531
+ parsed = JSON.parse(raw);
6532
+ } catch {
6533
+ this.clear(key);
6534
+ return null;
6535
+ }
6536
+ if (!isPersistedShape(parsed)) {
6537
+ this.clear(key);
6538
+ return null;
6539
+ }
6540
+ return parsed;
6541
+ }
6542
+ save(key, session) {
6543
+ try {
6544
+ window.localStorage.setItem(key, JSON.stringify(session));
6545
+ } catch {
6546
+ }
6547
+ }
6548
+ clear(key) {
6549
+ try {
6550
+ window.localStorage.removeItem(key);
6551
+ } catch {
6552
+ }
6553
+ }
6554
+ };
6555
+ var MemorySessionStore = class {
6556
+ entries = /* @__PURE__ */ new Map();
6557
+ load(key) {
6558
+ return this.entries.get(key) ?? null;
6559
+ }
6560
+ save(key, session) {
6561
+ this.entries.set(key, session);
6562
+ }
6563
+ clear(key) {
6564
+ this.entries.delete(key);
6565
+ }
6566
+ };
6567
+ function localStorageUsable() {
6568
+ try {
6569
+ if (typeof window === "undefined" || !window.localStorage)
6570
+ return false;
6571
+ const probe = "__dh_server_session_probe__";
6572
+ window.localStorage.setItem(probe, "1");
6573
+ window.localStorage.removeItem(probe);
6574
+ return true;
6575
+ } catch {
6576
+ return false;
6577
+ }
6578
+ }
6579
+ function createDefaultSessionStore() {
6580
+ return localStorageUsable() ? new LocalStorageSessionStore() : new MemorySessionStore();
6581
+ }
6582
+
5827
6583
  // src/utils/server-session.ts
5828
6584
  var REFRESH_LEEWAY_SECONDS = 30;
6585
+ var ServerLoginError = class extends Error {
6586
+ constructor(message, status) {
6587
+ super(message);
6588
+ this.status = status;
6589
+ this.name = "ServerLoginError";
6590
+ }
6591
+ };
6592
+ function isLoginRejection(error) {
6593
+ return error instanceof ServerLoginError && error.status >= 400 && error.status < 500 && error.status !== 429;
6594
+ }
5829
6595
  var ServerSession = class {
5830
6596
  signer;
5831
6597
  serviceEndpoint;
5832
6598
  chainId;
6599
+ store;
6600
+ onSignaturePrompt;
5833
6601
  now;
5834
6602
  fetchImpl;
5835
6603
  cached = null;
5836
6604
  inFlight = null;
6605
+ /**
6606
+ * Bumped by `clearPersisted()` (logout / disconnect). An in-flight
6607
+ * `getOrRefresh()` captures this at start and refuses to write its freshly
6608
+ * minted token back to `this.cached`/the store if the value changed
6609
+ * meanwhile — so a refresh racing a sign-out can't silently "un-clear" the
6610
+ * credential it was told to drop.
6611
+ */
6612
+ generation = 0;
6613
+ /**
6614
+ * Store key for the current signer, memoized on first use so the sync
6615
+ * `clear()` can drop the persisted token without an async address lookup.
6616
+ */
6617
+ lastStoreKey = null;
5837
6618
  constructor(opts) {
5838
6619
  assertSafeServiceEndpoint(opts.serviceEndpoint);
5839
6620
  this.signer = opts.signer;
5840
6621
  this.serviceEndpoint = opts.serviceEndpoint.replace(/\/+$/, "");
5841
6622
  this.chainId = opts.chainId;
6623
+ this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6624
+ this.onSignaturePrompt = opts.onSignaturePrompt;
5842
6625
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
5843
6626
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
5844
6627
  }
5845
- /** Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds. */
5846
- async getValidToken() {
6628
+ /**
6629
+ * Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds.
6630
+ * Pass `{ silent: true }` to forbid a fresh wallet signature: it resolves ONLY
6631
+ * from a live cached token or the persisted re-mint envelope, never prompting
6632
+ * and never joining a prompt-capable in-flight refresh (used by `logout()`,
6633
+ * which must never pop the wallet during sign-out). Rejects when only a fresh
6634
+ * signature could produce a token.
6635
+ */
6636
+ async getValidToken(opts) {
6637
+ if (opts?.silent) {
6638
+ const session2 = await this.tryResolveSilently(this.generation);
6639
+ if (!session2) {
6640
+ throw new Error("ServerSession: no existing session to refresh");
6641
+ }
6642
+ return session2.token;
6643
+ }
5847
6644
  const session = await this.getOrRefresh();
5848
6645
  return session.token;
5849
6646
  }
@@ -5851,19 +6648,61 @@ var ServerSession = class {
5851
6648
  async getAuthHeader() {
5852
6649
  return { Authorization: `Bearer ${await this.getValidToken()}` };
5853
6650
  }
5854
- /** Drop the cached token (e.g. on 401 from server). Next call re-logs in. */
6651
+ /**
6652
+ * Drop the cached token (e.g. on 401 from server). The persisted envelope is
6653
+ * kept — the next call re-mints silently — but its stored token copy is
6654
+ * dropped too so a dead JWT can't be re-adopted from the store.
6655
+ */
5855
6656
  clear() {
5856
6657
  this.cached = null;
5857
6658
  this.inFlight = null;
6659
+ if (!this.store || !this.lastStoreKey)
6660
+ return;
6661
+ const persisted = this.store.load(this.lastStoreKey);
6662
+ if (persisted?.token) {
6663
+ this.store.save(this.lastStoreKey, {
6664
+ ...persisted,
6665
+ token: void 0,
6666
+ tokenExpiresAt: void 0
6667
+ });
6668
+ }
6669
+ }
6670
+ /**
6671
+ * Drop the session AND the persisted re-mint envelope (wallet disconnect /
6672
+ * explicit sign-out paths — anything that must remove the 24h credential
6673
+ * from storage).
6674
+ */
6675
+ async clearPersisted() {
6676
+ this.generation += 1;
6677
+ this.cached = null;
6678
+ this.inFlight = null;
6679
+ if (!this.store)
6680
+ return;
6681
+ try {
6682
+ if (this.lastStoreKey) {
6683
+ this.store.clear(this.lastStoreKey);
6684
+ } else {
6685
+ const address = (await this.signer.getAddress()).toLowerCase();
6686
+ this.store.clear(
6687
+ sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6688
+ );
6689
+ }
6690
+ } catch {
6691
+ } finally {
6692
+ this.generation += 1;
6693
+ this.cached = null;
6694
+ }
5858
6695
  }
5859
6696
  /**
5860
- * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`).
5861
- * Clears the local cache regardless of server response so the client stops
5862
- * presenting the token.
6697
+ * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`)
6698
+ * which also tombstones the envelope's re-mint window server-side and
6699
+ * remove the persisted credential locally. Clears the local cache regardless
6700
+ * of server response so the client stops presenting the token.
5863
6701
  */
5864
6702
  async logout() {
5865
- const token = this.cached?.token ?? await this.getValidToken().catch(() => null);
5866
- this.clear();
6703
+ const cached = this.cached;
6704
+ const token = cached && cached.expiresAt > this.now() ? cached.token : await this.getValidToken({ silent: true }).catch(() => null);
6705
+ await this.clearPersisted();
5867
6706
  if (!token)
5868
6707
  return;
5869
6708
  const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/logout`, {
@@ -5877,6 +6716,50 @@ var ServerSession = class {
5877
6716
  );
5878
6717
  }
5879
6718
  }
6719
+ /**
6720
+ * Resolve a session WITHOUT ever prompting for a wallet signature: a live
6721
+ * cached token, else the persisted JWT (adopted if still fresh, otherwise
6722
+ * silently re-minted from the envelope). Returns `null` only when a fresh
6723
+ * wallet signature would be required (dead/absent envelope). Never touches
6724
+ * `this.inFlight`, so a silent caller (logout) can resolve independently
6725
+ * instead of being dragged into a prompt-capable refresh already in flight.
6726
+ *
6727
+ * `gen` is the caller's clear-generation snapshot: writes to `this.cached`
6728
+ * and the store are skipped if a `clearPersisted()` landed meanwhile.
6729
+ */
6730
+ async tryResolveSilently(gen) {
6731
+ const nowSec = this.now();
6732
+ if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
6733
+ return this.cached;
6734
+ }
6735
+ const persisted = await this.loadPersisted();
6736
+ if (!persisted)
6737
+ return null;
6738
+ if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6739
+ const adopted = {
6740
+ token: persisted.token,
6741
+ expiresAt: persisted.tokenExpiresAt
6742
+ };
6743
+ if (gen === this.generation)
6744
+ this.cached = adopted;
6745
+ return adopted;
6746
+ }
6747
+ try {
6748
+ const session = await this.login(persisted.payload);
6749
+ if (gen === this.generation) {
6750
+ this.persist(persisted.payload, session);
6751
+ this.cached = session;
6752
+ }
6753
+ return session;
6754
+ } catch (error) {
6755
+ if (!isLoginRejection(error))
6756
+ throw error;
6757
+ if (gen === this.generation && this.store && this.lastStoreKey) {
6758
+ this.store.clear(this.lastStoreKey);
6759
+ }
6760
+ return null;
6761
+ }
6762
+ }
5880
6763
  async getOrRefresh() {
5881
6764
  const nowSec = this.now();
5882
6765
  if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
@@ -5884,39 +6767,97 @@ var ServerSession = class {
5884
6767
  }
5885
6768
  if (this.inFlight)
5886
6769
  return this.inFlight;
6770
+ const gen = this.generation;
6771
+ const notCleared = () => gen === this.generation;
5887
6772
  this.inFlight = (async () => {
5888
6773
  try {
6774
+ const existing = await this.tryResolveSilently(gen);
6775
+ if (existing)
6776
+ return existing;
6777
+ this.onSignaturePrompt?.();
5889
6778
  const payload = await buildSignedLoginPayload(
5890
6779
  this.signer,
5891
6780
  this.chainId,
5892
6781
  this.serviceEndpoint
5893
6782
  );
5894
- const res = await this.fetchImpl(
5895
- `${this.serviceEndpoint}/api/auth/login`,
5896
- {
5897
- method: "POST",
5898
- headers: { "Content-Type": "application/json" },
5899
- body: JSON.stringify(payload)
5900
- }
5901
- );
5902
- if (!res.ok) {
5903
- const detail = await res.text().catch(() => "");
5904
- throw new Error(
5905
- `lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`
5906
- );
6783
+ const session = await this.login(payload);
6784
+ if (notCleared()) {
6785
+ this.persist(payload, session);
6786
+ this.cached = session;
5907
6787
  }
5908
- const body = await res.json();
5909
- if (!body?.token || !body?.expiresAt) {
5910
- throw new Error("lit-ops-server login returned malformed response");
5911
- }
5912
- this.cached = { token: body.token, expiresAt: body.expiresAt };
5913
- return this.cached;
6788
+ return session;
5914
6789
  } finally {
5915
- this.inFlight = null;
6790
+ if (notCleared())
6791
+ this.inFlight = null;
5916
6792
  }
5917
6793
  })();
5918
6794
  return this.inFlight;
5919
6795
  }
6796
+ async login(payload) {
6797
+ const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/login`, {
6798
+ method: "POST",
6799
+ headers: { "Content-Type": "application/json" },
6800
+ body: JSON.stringify(payload)
6801
+ });
6802
+ if (!res.ok) {
6803
+ const detail = await res.text().catch(() => "");
6804
+ throw new ServerLoginError(
6805
+ `lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`,
6806
+ res.status
6807
+ );
6808
+ }
6809
+ const body = await res.json();
6810
+ if (!body?.token || !body?.expiresAt) {
6811
+ throw new Error("lit-ops-server login returned malformed response");
6812
+ }
6813
+ return { token: body.token, expiresAt: body.expiresAt };
6814
+ }
6815
+ /**
6816
+ * Load the persisted session for the current signer, validating that it
6817
+ * belongs to this address/chain/endpoint and that the envelope is still
6818
+ * inside the re-auth window (with a safety margin). Invalid or expired
6819
+ * entries are evicted.
6820
+ */
6821
+ async loadPersisted() {
6822
+ if (!this.store)
6823
+ return null;
6824
+ try {
6825
+ const address = (await this.signer.getAddress()).toLowerCase();
6826
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6827
+ this.lastStoreKey = key;
6828
+ const persisted = this.store.load(key);
6829
+ if (!persisted)
6830
+ return null;
6831
+ const withinWindow = this.now() < persisted.payload.message.issuedAt + REAUTH_WINDOW_SECONDS - SAFETY_MARGIN_SECONDS;
6832
+ if (persisted.address !== address || persisted.chainId !== this.chainId || persisted.endpoint !== this.serviceEndpoint || persisted.payload.message.address.toLowerCase() !== address || !withinWindow) {
6833
+ this.store.clear(key);
6834
+ return null;
6835
+ }
6836
+ return persisted;
6837
+ } catch {
6838
+ return null;
6839
+ }
6840
+ }
6841
+ /** Best-effort save of the envelope + live JWT for silent recovery. */
6842
+ persist(payload, session) {
6843
+ if (!this.store)
6844
+ return;
6845
+ const address = payload.message.address.toLowerCase();
6846
+ try {
6847
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6848
+ this.lastStoreKey = key;
6849
+ this.store.save(key, {
6850
+ version: 1,
6851
+ address,
6852
+ chainId: this.chainId,
6853
+ endpoint: this.serviceEndpoint,
6854
+ payload,
6855
+ token: session.token,
6856
+ tokenExpiresAt: session.expiresAt
6857
+ });
6858
+ } catch {
6859
+ }
6860
+ }
5920
6861
  };
5921
6862
 
5922
6863
  // src/utils/quantum-revert.utils.ts
@@ -6039,6 +6980,36 @@ async function assertSafeQuantumSubmission(opts) {
6039
6980
  var import_ethers6 = require("ethers");
6040
6981
  var QUANTUM_SECONDS = 60;
6041
6982
  var QUANTUM_SAFE_THRESHOLD = 40;
6983
+ function hashBtcInputSet(utxos) {
6984
+ if (!Array.isArray(utxos) || utxos.length === 0) {
6985
+ throw new Error("hashBtcInputSet: at least one input is required");
6986
+ }
6987
+ const normalized = utxos.map((u) => {
6988
+ const txid = String(u.txid).toLowerCase().replace(/^0x/, "");
6989
+ const vout = Number(u.vout);
6990
+ const value = Number(u.value);
6991
+ if (!/^[0-9a-f]{64}$/.test(txid)) {
6992
+ throw new Error(`hashBtcInputSet: invalid txid "${u.txid}"`);
6993
+ }
6994
+ if (!Number.isSafeInteger(vout) || vout < 0) {
6995
+ throw new Error(`hashBtcInputSet: invalid vout ${u.vout} for ${txid}`);
6996
+ }
6997
+ if (!Number.isSafeInteger(value) || value <= 0) {
6998
+ throw new Error(`hashBtcInputSet: invalid value ${u.value} for ${txid}:${vout}`);
6999
+ }
7000
+ return { txid, vout, value };
7001
+ });
7002
+ const seen = /* @__PURE__ */ new Set();
7003
+ for (const u of normalized) {
7004
+ const key = `${u.txid}:${u.vout}`;
7005
+ if (seen.has(key)) {
7006
+ throw new Error(`hashBtcInputSet: duplicate outpoint ${key}`);
7007
+ }
7008
+ seen.add(key);
7009
+ }
7010
+ const canonical = normalized.sort((a, b) => a.txid < b.txid ? -1 : a.txid > b.txid ? 1 : a.vout - b.vout).map((u) => `${u.txid}:${u.vout}:${u.value}`).join("|");
7011
+ return (0, import_ethers6.keccak256)((0, import_ethers6.toUtf8Bytes)(canonical));
7012
+ }
6042
7013
  async function buildBtcExecuteEnvelope(params) {
6043
7014
  const {
6044
7015
  positionId,
@@ -6059,6 +7030,17 @@ async function buildBtcExecuteEnvelope(params) {
6059
7030
  "buildBtcExecuteEnvelope: targetAmount must be a positive number of satoshis"
6060
7031
  );
6061
7032
  }
7033
+ const utxos = params.utxos && params.utxos.length > 0 ? params.utxos : [{ txid, vout, value: satoshis }];
7034
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
7035
+ const authorizedInSet = utxos.some(
7036
+ (u) => normTxid(u.txid) === normTxid(txid) && Number(u.vout) === Number(vout)
7037
+ );
7038
+ if (!authorizedInSet) {
7039
+ throw new Error(
7040
+ `buildBtcExecuteEnvelope: the authorized UTXO ${txid}:${vout} must be part of utxos[]`
7041
+ );
7042
+ }
7043
+ const utxosHash = hashBtcInputSet(utxos);
6062
7044
  const nowSec = Date.now() / 1e3;
6063
7045
  const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
6064
7046
  const secsIntoQuantum = nowSec - currentQuantumStart;
@@ -6095,6 +7077,8 @@ async function buildBtcExecuteEnvelope(params) {
6095
7077
  // targetAddress (BTC destination)
6096
7078
  "uint256",
6097
7079
  // targetAmount (sats authorized to recipient)
7080
+ "bytes32",
7081
+ // utxosHash (canonical hash of the FULL input set — P6/#8)
6098
7082
  "bytes32"
6099
7083
  // actionHash
6100
7084
  ],
@@ -6106,6 +7090,7 @@ async function buildBtcExecuteEnvelope(params) {
6106
7090
  networkFee,
6107
7091
  targetAddress,
6108
7092
  targetAmount,
7093
+ utxosHash,
6109
7094
  actionHash
6110
7095
  ]
6111
7096
  );
@@ -6122,7 +7107,8 @@ async function buildBtcExecuteEnvelope(params) {
6122
7107
  chainId,
6123
7108
  timestamp,
6124
7109
  userSignature,
6125
- borrowerAddress
7110
+ borrowerAddress,
7111
+ utxos
6126
7112
  };
6127
7113
  }
6128
7114
 
@@ -8938,6 +9924,56 @@ var LoanQuery = class {
8938
9924
  })
8939
9925
  );
8940
9926
  }
9927
+ /**
9928
+ * Get loan-event history for every position owned by a wallet.
9929
+ *
9930
+ * Composes getUserPositions + getLoanEvents (the subgraph has no wallet-scoped
9931
+ * event collection). Each returned LoanEvents carries its own positionId, so
9932
+ * callers can flatten and merge into a single per-wallet timeline.
9933
+ *
9934
+ * Bitcoin deposits never appear in the subgraph — merge those in from an
9935
+ * Esplora source on the consumer side.
9936
+ *
9937
+ * @param userAddress - Loan-owner wallet address.
9938
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
9939
+ * @returns One LoanEvents object per position (empty array if the wallet has none).
9940
+ *
9941
+ * @example
9942
+ * ```typescript
9943
+ * const result = await loanQuery.getWalletLoanEvents('0x830b...');
9944
+ * if (result.success) {
9945
+ * const allEvents = result.value; // LoanEvents[] — one per position
9946
+ * }
9947
+ * ```
9948
+ */
9949
+ async getWalletLoanEvents(userAddress, filter) {
9950
+ if (this.config.debug) {
9951
+ log.info("\u{1F50D} Querying wallet loan events", { userAddress, filter });
9952
+ }
9953
+ return tryCatchAsync(
9954
+ async () => {
9955
+ const perPosition = await this.config.graphClient.getWalletLoanEvents(
9956
+ userAddress,
9957
+ filter
9958
+ );
9959
+ if (this.config.debug) {
9960
+ log.info("\u2705 Retrieved wallet loan events", {
9961
+ userAddress,
9962
+ positions: perPosition.length,
9963
+ totalEvents: perPosition.reduce((n, p) => n + p.totalEvents, 0)
9964
+ });
9965
+ }
9966
+ return perPosition;
9967
+ },
9968
+ (error) => new SDKError({
9969
+ message: "Failed to query wallet loan events",
9970
+ category: "SUBGRAPH" /* SUBGRAPH */,
9971
+ severity: "MEDIUM" /* MEDIUM */,
9972
+ originalError: error instanceof Error ? error : new Error(String(error)),
9973
+ context: { userAddress, filter }
9974
+ })
9975
+ );
9976
+ }
8941
9977
  /**
8942
9978
  * Clear loan cache
8943
9979
  *
@@ -11679,6 +12715,39 @@ var DiamondHandsGraph = class {
11679
12715
  totalEvents
11680
12716
  };
11681
12717
  }
12718
+ /**
12719
+ * Get the full loan-event history for every position owned by a wallet.
12720
+ *
12721
+ * The subgraph has no wallet-scoped event collection (`User` only derives
12722
+ * `positions`), so this composes the two existing wallet/position queries:
12723
+ * `getUserPositions(wallet)` to enumerate the wallet's positions, then
12724
+ * `getLoanEvents(positionId, filter)` per position. Each returned `LoanEvents`
12725
+ * already carries its own `positionId`, so callers can flatten and merge into
12726
+ * a single timeline.
12727
+ *
12728
+ * Note: Bitcoin deposits are UTXOs on the Bitcoin network and never appear in
12729
+ * the EVM subgraph — surface those from an Esplora source and merge separately.
12730
+ *
12731
+ * @param userAddress - The loan-owner wallet address.
12732
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
12733
+ * @returns One LoanEvents object per position; empty array if the wallet has none.
12734
+ */
12735
+ async getWalletLoanEvents(userAddress, filter) {
12736
+ const { positions } = await this.getUserPositions(userAddress);
12737
+ const positionIds = positions.map((p) => p?.id != null ? String(p.id) : "").filter((id) => id.length > 0);
12738
+ if (positionIds.length === 0)
12739
+ return [];
12740
+ const CONCURRENCY = 5;
12741
+ const results = [];
12742
+ for (let i = 0; i < positionIds.length; i += CONCURRENCY) {
12743
+ const batch = positionIds.slice(i, i + CONCURRENCY);
12744
+ const settled = await Promise.all(
12745
+ batch.map((positionId) => this.getLoanEvents(positionId, filter))
12746
+ );
12747
+ results.push(...settled);
12748
+ }
12749
+ return results;
12750
+ }
11682
12751
  // ============================================================================
11683
12752
  // Protocol-wide Events
11684
12753
  // ============================================================================
@@ -15091,6 +16160,27 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
15091
16160
  return {};
15092
16161
  return this.serverSession.getAuthHeader();
15093
16162
  }
16163
+ /**
16164
+ * Sign out of the lit-ops-server session for the current auth signer. Call on
16165
+ * wallet disconnect or explicit sign-out so the credential does not outlive
16166
+ * the wallet connection. No-op in standalone mode.
16167
+ *
16168
+ * Attempts a server-side revocation first (`ServerSession.logout()` — which
16169
+ * tombstones the envelope's 24h re-mint window and invalidates the JWT), then
16170
+ * always removes the local credential. If the revocation call fails (offline,
16171
+ * server down, wallet already disconnected) we still clear locally so the
16172
+ * session cannot silently re-mint after sign-out; `logout()` is best-effort
16173
+ * and never prompts the wallet.
16174
+ */
16175
+ async clearServerSession() {
16176
+ if (!this.serverSession)
16177
+ return;
16178
+ try {
16179
+ await this.serverSession.logout();
16180
+ } catch {
16181
+ await this.serverSession.clearPersisted();
16182
+ }
16183
+ }
15094
16184
  /**
15095
16185
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
15096
16186
  * the post-write state. We clear the entire loan-query cache (not just
@@ -15202,7 +16292,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
15202
16292
  this.serverSession = new ServerSession({
15203
16293
  signer: config.authSigner ?? config.contractSigner,
15204
16294
  serviceEndpoint: config.serviceEndpoint,
15205
- chainId: config.chainId
16295
+ chainId: config.chainId,
16296
+ persistSession: config.sessionPersistence?.enabled,
16297
+ sessionStore: config.sessionPersistence?.store,
16298
+ onSignaturePrompt: config.onSessionSignaturePrompt
15206
16299
  });
15207
16300
  }
15208
16301
  const contractManagerResult = createContractManager({
@@ -17843,9 +18936,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
17843
18936
  userSignature: auth.signature,
17844
18937
  rpcUrl: _rpcUrl || this.config.ethRpcUrl,
17845
18938
  // Forward a Bitcoin provider so the validator reaches a reachable esplora on Hardhat
17846
- // (lit-ops maps it to the validator's devBitcoinProviderUrl); ignored off-hardhat. Prefer
18939
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
18940
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
18941
+ // is resolved from the on-chain BitcoinProviderRegistry and this field is server-ignored
18942
+ // — emitting it there only misleads readers into thinking it is caller-controlled. Prefer
17847
18943
  // a per-request override (mirrors the mint path's customBitcoinRpcUrl) over the config default.
17848
- ...(_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
18944
+ ...this.config.chain === "hardhat" && (_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
17849
18945
  bitcoinProviderUrl: _bitcoinRpcUrl || this.config.bitcoinProviders[0].url
17850
18946
  },
17851
18947
  ...this.config.priceProviders && {
@@ -18626,9 +19722,10 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
18626
19722
  rpcUrl: this.config.ethRpcUrl,
18627
19723
  // Pass the SDK's RPC URL to LIT Action
18628
19724
  ...request.rpcUrl && { rpcUrl: request.rpcUrl },
18629
- // Forward the configured Bitcoin provider so the validator reaches a reachable esplora on
18630
- // Hardhat (lit-ops maps it to the validator's devBitcoinProviderUrl); ignored off-hardhat.
18631
- ...this.config.bitcoinProviders?.[0]?.url && {
19725
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
19726
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
19727
+ // comes from the on-chain BitcoinProviderRegistry and this field is server-ignored.
19728
+ ...this.config.chain === "hardhat" && this.config.bitcoinProviders?.[0]?.url && {
18632
19729
  bitcoinProviderUrl: this.config.bitcoinProviders[0].url
18633
19730
  },
18634
19731
  ...this.config.priceProviders && {
@@ -19687,6 +20784,42 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19687
20784
  };
19688
20785
  }
19689
20786
  }
20787
+ /**
20788
+ * TEMPORARY (multi-UTXO withdrawal fix, Phase A): fetch the vault's confirmed
20789
+ * UTXO set from lit-ops-server (`GET /api/lit/vault-utxos`) so the caller can
20790
+ * assemble and sign the FULL input set for a multi-UTXO consolidation
20791
+ * withdrawal. Phase 1 only returns ONE representative UTXO, so a vault whose
20792
+ * balance is spread across several UTXOs otherwise fails the Phase-2 signer
20793
+ * with "Insufficient UTXO value ... across 1 input(s)".
20794
+ *
20795
+ * Returns `[]` on any failure (non-service mode, network error, bad shape) so
20796
+ * callers transparently fall back to the single authorized UTXO. Superseded
20797
+ * by the btc-withdrawal Lit Action returning its own confirmedUTXOs (Phase B).
20798
+ */
20799
+ async fetchConfirmedVaultUtxos(positionId) {
20800
+ if (this.config.mode !== "service" || !this.config.serviceEndpoint)
20801
+ return [];
20802
+ try {
20803
+ const pid = positionId.startsWith("0x") ? positionId : `0x${positionId}`;
20804
+ const resp = await fetch(
20805
+ `${this.config.serviceEndpoint}/api/lit/vault-utxos?positionId=${encodeURIComponent(pid)}`,
20806
+ { method: "GET", headers: { ...await this.getAuthHeader() } }
20807
+ );
20808
+ if (!resp.ok)
20809
+ return [];
20810
+ const json = await resp.json();
20811
+ if (!json.success || !Array.isArray(json.data?.utxos))
20812
+ return [];
20813
+ return json.data.utxos.map((u) => ({
20814
+ txid: String(u.txid),
20815
+ vout: Number(u.vout),
20816
+ value: Number(u.value),
20817
+ confirmations: Number(u.confirmations ?? 0)
20818
+ }));
20819
+ } catch {
20820
+ return [];
20821
+ }
20822
+ }
19690
20823
  /**
19691
20824
  * Execute Bitcoin withdrawal (Phase 2)
19692
20825
  *
@@ -19756,7 +20889,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19756
20889
  targetAmount: request.targetAmount,
19757
20890
  networkFee: request.networkFee,
19758
20891
  chainId,
19759
- signer
20892
+ signer,
20893
+ ...request.utxos ? { utxos: request.utxos } : {}
19760
20894
  });
19761
20895
  const headers = {
19762
20896
  "Content-Type": "application/json",
@@ -19787,7 +20921,10 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19787
20921
  userSignature: envelope.userSignature,
19788
20922
  borrowerAddress: envelope.borrowerAddress,
19789
20923
  chainId: envelope.chainId,
19790
- timestamp: envelope.timestamp
20924
+ timestamp: envelope.timestamp,
20925
+ // P6/#8: the exact input set hashed into the user signature.
20926
+ // The server must forward this set verbatim to the signer.
20927
+ utxos: envelope.utxos
19791
20928
  })
19792
20929
  }
19793
20930
  );
@@ -20127,10 +21264,13 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20127
21264
  }
20128
21265
  },
20129
21266
  rpcUrl,
20130
- ...sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
21267
+ // Hardhat-ONLY: never emitted on prod chains (server-ignored there; provider comes
21268
+ // from the on-chain BitcoinProviderRegistry). The stale-spend action reads
21269
+ // `devBitcoinProviderUrl` directly on Hardhat — this legacy alias is redundant but
21270
+ // kept behind the Hardhat gate so no prod request carries the caller-looking field.
21271
+ ...this.config.chain === "hardhat" && sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
20131
21272
  // Hardhat (chainId 1337/31337) has no on-chain BitcoinProviderRegistry —
20132
- // Lit Actions read `devBitcoinProviderUrl` from globalThis. Always pass;
20133
- // non-hardhat chains ignore it. (Single source of truth: bitcoinProviderUrl.)
21273
+ // Lit Actions read `devBitcoinProviderUrl` from globalThis. Non-hardhat chains ignore it.
20134
21274
  ...sdkBitcoinProviderUrl && { devBitcoinProviderUrl: sdkBitcoinProviderUrl },
20135
21275
  ...params.minConfirmations !== void 0 && {
20136
21276
  minConfirmations: params.minConfirmations
@@ -20296,13 +21436,44 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20296
21436
  if (this.config.debug) {
20297
21437
  log.info("\u{1F50D} Found vault UTXO for execution", { utxoIdentifier });
20298
21438
  }
21439
+ let executionUtxos;
21440
+ const gatheredUtxos = await this.fetchConfirmedVaultUtxos(positionId);
21441
+ if (gatheredUtxos.length > 0) {
21442
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
21443
+ const byOutpoint = /* @__PURE__ */ new Map();
21444
+ for (const u of gatheredUtxos) {
21445
+ const txid = normTxid(u.txid);
21446
+ byOutpoint.set(`${txid}:${u.vout}`, {
21447
+ txid,
21448
+ vout: Number(u.vout),
21449
+ value: Number(u.value)
21450
+ });
21451
+ }
21452
+ const repKey = `${normTxid(authResult.utxoTxid)}:${authResult.utxoVout}`;
21453
+ if (!byOutpoint.has(repKey)) {
21454
+ byOutpoint.set(repKey, {
21455
+ txid: normTxid(authResult.utxoTxid),
21456
+ vout: Number(authResult.utxoVout),
21457
+ value: Number(authResult.utxoSatoshis)
21458
+ });
21459
+ }
21460
+ if (byOutpoint.size > 1) {
21461
+ executionUtxos = Array.from(byOutpoint.values());
21462
+ if (this.config.debug) {
21463
+ log.info("\u{1F9E9} Multi-UTXO withdrawal: signing full input set", {
21464
+ inputs: executionUtxos.length
21465
+ });
21466
+ }
21467
+ }
21468
+ }
20299
21469
  const executionResult = await this.executeBTCWithdrawal({
20300
21470
  positionId,
20301
21471
  utxoIdentifier,
20302
21472
  utxoSatoshis: Number(authResult.utxoSatoshis),
20303
21473
  networkFee,
20304
21474
  destination: withdrawalAddress,
20305
- targetAmount: withdrawalAmount
21475
+ targetAmount: withdrawalAmount,
21476
+ ...executionUtxos ? { utxos: executionUtxos } : {}
20306
21477
  });
20307
21478
  if (this.config.debug) {
20308
21479
  log.info("\u2705 Phase 2 complete: Bitcoin transaction executed", {
@@ -20453,6 +21624,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20453
21624
  async getLoanEvents(positionId, filter) {
20454
21625
  return this.loanQuery.getLoanEvents(positionId, filter);
20455
21626
  }
21627
+ /**
21628
+ * Get loan-event history for every position owned by a wallet.
21629
+ * Composes getUserPositions + getLoanEvents; returns one LoanEvents per
21630
+ * position (each carrying its own positionId). Bitcoin deposits are not on the
21631
+ * subgraph — merge those in from an Esplora source on the consumer side.
21632
+ */
21633
+ async getWalletLoanEvents(userAddress, filter) {
21634
+ return this.loanQuery.getWalletLoanEvents(userAddress, filter);
21635
+ }
20456
21636
  /**
20457
21637
  * Get protocol-wide events across all indexed entity types.
20458
21638
  * Returns a merged, sorted discriminated-union timeline.
@@ -20909,6 +22089,38 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20909
22089
  }));
20910
22090
  }
20911
22091
  }
22092
+ /**
22093
+ * List a vault address's PENDING (not-yet-confirmed) incoming BTC deposits.
22094
+ *
22095
+ * A deposit is "pending" until it reaches the protocol's minimum confirmation
22096
+ * depth (6) — the confirmed balance endpoints (`getVaultBalance` /
22097
+ * `getAddressBalance`) omit anything shallower, so this surfaces in-flight
22098
+ * deposits (0–5 confirmations). Server-delegated: lit-ops-server queries its
22099
+ * configured premium Esplora provider (`/api/lit/pending-deposits`). Requires
22100
+ * service mode.
22101
+ */
22102
+ async getPendingDeposits(vaultAddress) {
22103
+ this.ensureInitialized();
22104
+ if (!isServiceModeConfig(this.config)) {
22105
+ throw new Error("getPendingDeposits requires service mode \u2014 standalone mode is not supported");
22106
+ }
22107
+ const trimmedAddress = vaultAddress?.trim();
22108
+ if (!trimmedAddress) {
22109
+ throw new Error("vaultAddress is required");
22110
+ }
22111
+ const serviceEndpoint = this.config.serviceEndpoint;
22112
+ const url = `${serviceEndpoint}/api/lit/pending-deposits?vaultAddress=${encodeURIComponent(trimmedAddress)}`;
22113
+ const response = await fetch(url, {
22114
+ headers: await this.getAuthHeader()
22115
+ });
22116
+ if (!response.ok) {
22117
+ throw new Error(`Pending deposits request failed: ${response.status} ${response.statusText}`);
22118
+ }
22119
+ const json = await response.json();
22120
+ if (!json.success)
22121
+ throw new Error(json.error ?? "Pending deposits request failed");
22122
+ return json.data ?? [];
22123
+ }
20912
22124
  // ============================================================================
20913
22125
  // HIGH-LEVEL API - Mock Token Operations (Test Networks Only)
20914
22126
  // ============================================================================
@@ -21404,6 +22616,20 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21404
22616
  if (this.config.debug) {
21405
22617
  log.info(`\u{1F310} Calling liquidation server endpoint: ${endpoint}`);
21406
22618
  }
22619
+ const signer = this.getSignerOrThrow();
22620
+ const intentSigner = await signer.getAddress();
22621
+ const chainId = Number(
22622
+ (await this.getProviderOrThrow().getNetwork()).chainId
22623
+ );
22624
+ const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
22625
+ const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
22626
+ const intentTimestamp = Math.floor(Date.now() / 1e3);
22627
+ const intentActionHash = (0, import_ethers14.keccak256)((0, import_ethers14.toUtf8Bytes)("liquidate-position"));
22628
+ const intentHash = (0, import_ethers14.solidityPackedKeccak256)(
22629
+ ["bytes32", "uint256", "uint256", "address", "bytes32"],
22630
+ [canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
22631
+ );
22632
+ const intentSignature = await signer.signMessage((0, import_ethers14.getBytes)(intentHash));
21407
22633
  const response = await fetch(endpoint, {
21408
22634
  method: "POST",
21409
22635
  headers: { "Content-Type": "application/json" },
@@ -21419,6 +22645,11 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21419
22645
  UCDController: params.contractAddresses.ucdController,
21420
22646
  LiquidationManagerModule: params.contractAddresses.liquidationManager
21421
22647
  },
22648
+ liquidatorIntent: {
22649
+ timestamp: intentTimestamp,
22650
+ chainId,
22651
+ signature: intentSignature
22652
+ },
21422
22653
  rpcUrl: params.rpcUrl || this.config.ethRpcUrl,
21423
22654
  priceProviders: this.config.priceProviders,
21424
22655
  pkpTokenId: params.pkpData.tokenId,