@gvnrdao/dh-sdk 0.0.301 → 0.0.303

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(
@@ -4032,52 +4051,622 @@ ${auth.clientId}`;
4032
4051
  console.log(
4033
4052
  `[Price Oracle] \u2705 [+${Date.now() - t}ms] ${source.name}: $${priceUSD.toLocaleString()}`
4034
4053
  );
4035
- return priceUSD;
4036
- } catch (error) {
4037
- const cause = error instanceof Error ? error.message : String(error);
4038
- console.log(`[Price Oracle] \u274C ${source.name}: ${cause}`);
4039
- throw new Error(`${source.name} price fetch failed: ${cause}`);
4054
+ return priceUSD;
4055
+ } catch (error) {
4056
+ const cause = error instanceof Error ? error.message : String(error);
4057
+ console.log(`[Price Oracle] \u274C ${source.name}: ${cause}`);
4058
+ throw new Error(`${source.name} price fetch failed: ${cause}`);
4059
+ }
4060
+ })
4061
+ );
4062
+ const prices = await Promise.race([fetchAll, timeoutPromise]);
4063
+ const sorted = [...prices].sort((a, b) => a - b);
4064
+ let median;
4065
+ if (this.devSingleProvider) {
4066
+ median = sorted[0];
4067
+ } else {
4068
+ median = sorted[1];
4069
+ const spread = (sorted[2] - sorted[0]) / sorted[0];
4070
+ if (spread > PRICE_ORACLE_TOLERANCE) {
4071
+ const names = this.sources.map((s) => s.name).join(", ");
4072
+ const formatted = sorted.map((p) => `$${p.toLocaleString()}`).join(", ");
4073
+ throw new Error(
4074
+ `Price consensus failed: prices not within ${PRICE_ORACLE_TOLERANCE * 100}% tolerance. Providers: [${names}]. Prices: [${formatted}]`
4075
+ );
4076
+ }
4077
+ }
4078
+ const priceCents = Math.round(median * 100);
4079
+ const priceWith8Decimals = BigInt(priceCents) * 1000000n;
4080
+ const totalElapsed = Date.now() - startTime;
4081
+ console.log(`[Price Oracle] Median price: $${median.toLocaleString()} \u2014 total time: ${totalElapsed}ms`);
4082
+ console.log(`[Price Oracle] Price with 8 decimals: ${priceWith8Decimals}`);
4083
+ return priceWith8Decimals;
4084
+ }
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
+ );
4040
4606
  }
4041
- })
4042
- );
4043
- const prices = await Promise.race([fetchAll, timeoutPromise]);
4044
- const sorted = [...prices].sort((a, b) => a - b);
4045
- let median;
4046
- if (this.devSingleProvider) {
4047
- median = sorted[0];
4048
- } else {
4049
- median = sorted[1];
4050
- const spread = (sorted[2] - sorted[0]) / sorted[0];
4051
- if (spread > PRICE_ORACLE_TOLERANCE) {
4052
- const names = this.sources.map((s) => s.name).join(", ");
4053
- const formatted = sorted.map((p) => `$${p.toLocaleString()}`).join(", ");
4054
4607
  throw new Error(
4055
- `Price consensus failed: prices not within ${PRICE_ORACLE_TOLERANCE * 100}% tolerance. Providers: [${names}]. Prices: [${formatted}]`
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`
4056
4615
  );
4057
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);
4058
4646
  }
4059
- const priceCents = Math.round(median * 100);
4060
- const priceWith8Decimals = BigInt(priceCents) * 1000000n;
4061
- const totalElapsed = Date.now() - startTime;
4062
- console.log(`[Price Oracle] Median price: $${median.toLocaleString()} \u2014 total time: ${totalElapsed}ms`);
4063
- console.log(`[Price Oracle] Price with 8 decimals: ${priceWith8Decimals}`);
4064
- return priceWith8Decimals;
4065
4647
  }
4066
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,21 +6503,117 @@ 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
+ * Store key for the current signer, memoized on first use so the sync
6607
+ * `clear()` can drop the persisted token without an async address lookup.
6608
+ */
6609
+ lastStoreKey = null;
5837
6610
  constructor(opts) {
5838
6611
  assertSafeServiceEndpoint(opts.serviceEndpoint);
5839
6612
  this.signer = opts.signer;
5840
6613
  this.serviceEndpoint = opts.serviceEndpoint.replace(/\/+$/, "");
5841
6614
  this.chainId = opts.chainId;
6615
+ this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6616
+ this.onSignaturePrompt = opts.onSignaturePrompt;
5842
6617
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
5843
6618
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
5844
6619
  }
@@ -5851,19 +6626,56 @@ var ServerSession = class {
5851
6626
  async getAuthHeader() {
5852
6627
  return { Authorization: `Bearer ${await this.getValidToken()}` };
5853
6628
  }
5854
- /** Drop the cached token (e.g. on 401 from server). Next call re-logs in. */
6629
+ /**
6630
+ * Drop the cached token (e.g. on 401 from server). The persisted envelope is
6631
+ * kept — the next call re-mints silently — but its stored token copy is
6632
+ * dropped too so a dead JWT can't be re-adopted from the store.
6633
+ */
5855
6634
  clear() {
5856
6635
  this.cached = null;
5857
6636
  this.inFlight = null;
6637
+ if (!this.store || !this.lastStoreKey)
6638
+ return;
6639
+ const persisted = this.store.load(this.lastStoreKey);
6640
+ if (persisted?.token) {
6641
+ this.store.save(this.lastStoreKey, {
6642
+ ...persisted,
6643
+ token: void 0,
6644
+ tokenExpiresAt: void 0
6645
+ });
6646
+ }
6647
+ }
6648
+ /**
6649
+ * Drop the session AND the persisted re-mint envelope (wallet disconnect /
6650
+ * explicit sign-out paths — anything that must remove the 24h credential
6651
+ * from storage).
6652
+ */
6653
+ async clearPersisted() {
6654
+ this.cached = null;
6655
+ this.inFlight = null;
6656
+ if (!this.store)
6657
+ return;
6658
+ if (this.lastStoreKey) {
6659
+ this.store.clear(this.lastStoreKey);
6660
+ return;
6661
+ }
6662
+ try {
6663
+ const address = (await this.signer.getAddress()).toLowerCase();
6664
+ this.store.clear(
6665
+ sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6666
+ );
6667
+ } catch {
6668
+ }
5858
6669
  }
5859
6670
  /**
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.
6671
+ * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`)
6672
+ * which also tombstones the envelope's re-mint window server-side and
6673
+ * remove the persisted credential locally. Clears the local cache regardless
6674
+ * of server response so the client stops presenting the token.
5863
6675
  */
5864
6676
  async logout() {
5865
6677
  const token = this.cached?.token ?? await this.getValidToken().catch(() => null);
5866
- this.clear();
6678
+ await this.clearPersisted();
5867
6679
  if (!token)
5868
6680
  return;
5869
6681
  const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/logout`, {
@@ -5886,37 +6698,109 @@ var ServerSession = class {
5886
6698
  return this.inFlight;
5887
6699
  this.inFlight = (async () => {
5888
6700
  try {
6701
+ const persisted = await this.loadPersisted();
6702
+ if (persisted) {
6703
+ if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6704
+ this.cached = {
6705
+ token: persisted.token,
6706
+ expiresAt: persisted.tokenExpiresAt
6707
+ };
6708
+ return this.cached;
6709
+ }
6710
+ try {
6711
+ const session2 = await this.login(persisted.payload);
6712
+ this.persist(persisted.payload, session2);
6713
+ this.cached = session2;
6714
+ return session2;
6715
+ } catch (error) {
6716
+ if (!isLoginRejection(error))
6717
+ throw error;
6718
+ if (this.store && this.lastStoreKey) {
6719
+ this.store.clear(this.lastStoreKey);
6720
+ }
6721
+ }
6722
+ }
6723
+ this.onSignaturePrompt?.();
5889
6724
  const payload = await buildSignedLoginPayload(
5890
6725
  this.signer,
5891
6726
  this.chainId,
5892
6727
  this.serviceEndpoint
5893
6728
  );
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
- );
5907
- }
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;
6729
+ const session = await this.login(payload);
6730
+ this.persist(payload, session);
6731
+ this.cached = session;
6732
+ return session;
5914
6733
  } finally {
5915
6734
  this.inFlight = null;
5916
6735
  }
5917
6736
  })();
5918
6737
  return this.inFlight;
5919
6738
  }
6739
+ async login(payload) {
6740
+ const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/login`, {
6741
+ method: "POST",
6742
+ headers: { "Content-Type": "application/json" },
6743
+ body: JSON.stringify(payload)
6744
+ });
6745
+ if (!res.ok) {
6746
+ const detail = await res.text().catch(() => "");
6747
+ throw new ServerLoginError(
6748
+ `lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`,
6749
+ res.status
6750
+ );
6751
+ }
6752
+ const body = await res.json();
6753
+ if (!body?.token || !body?.expiresAt) {
6754
+ throw new Error("lit-ops-server login returned malformed response");
6755
+ }
6756
+ return { token: body.token, expiresAt: body.expiresAt };
6757
+ }
6758
+ /**
6759
+ * Load the persisted session for the current signer, validating that it
6760
+ * belongs to this address/chain/endpoint and that the envelope is still
6761
+ * inside the re-auth window (with a safety margin). Invalid or expired
6762
+ * entries are evicted.
6763
+ */
6764
+ async loadPersisted() {
6765
+ if (!this.store)
6766
+ return null;
6767
+ try {
6768
+ const address = (await this.signer.getAddress()).toLowerCase();
6769
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6770
+ this.lastStoreKey = key;
6771
+ const persisted = this.store.load(key);
6772
+ if (!persisted)
6773
+ return null;
6774
+ const withinWindow = this.now() < persisted.payload.message.issuedAt + REAUTH_WINDOW_SECONDS - SAFETY_MARGIN_SECONDS;
6775
+ if (persisted.address !== address || persisted.chainId !== this.chainId || persisted.endpoint !== this.serviceEndpoint || persisted.payload.message.address.toLowerCase() !== address || !withinWindow) {
6776
+ this.store.clear(key);
6777
+ return null;
6778
+ }
6779
+ return persisted;
6780
+ } catch {
6781
+ return null;
6782
+ }
6783
+ }
6784
+ /** Best-effort save of the envelope + live JWT for silent recovery. */
6785
+ persist(payload, session) {
6786
+ if (!this.store)
6787
+ return;
6788
+ const address = payload.message.address.toLowerCase();
6789
+ try {
6790
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6791
+ this.lastStoreKey = key;
6792
+ this.store.save(key, {
6793
+ version: 1,
6794
+ address,
6795
+ chainId: this.chainId,
6796
+ endpoint: this.serviceEndpoint,
6797
+ payload,
6798
+ token: session.token,
6799
+ tokenExpiresAt: session.expiresAt
6800
+ });
6801
+ } catch {
6802
+ }
6803
+ }
5920
6804
  };
5921
6805
 
5922
6806
  // src/utils/quantum-revert.utils.ts
@@ -6039,6 +6923,36 @@ async function assertSafeQuantumSubmission(opts) {
6039
6923
  var import_ethers6 = require("ethers");
6040
6924
  var QUANTUM_SECONDS = 60;
6041
6925
  var QUANTUM_SAFE_THRESHOLD = 40;
6926
+ function hashBtcInputSet(utxos) {
6927
+ if (!Array.isArray(utxos) || utxos.length === 0) {
6928
+ throw new Error("hashBtcInputSet: at least one input is required");
6929
+ }
6930
+ const normalized = utxos.map((u) => {
6931
+ const txid = String(u.txid).toLowerCase().replace(/^0x/, "");
6932
+ const vout = Number(u.vout);
6933
+ const value = Number(u.value);
6934
+ if (!/^[0-9a-f]{64}$/.test(txid)) {
6935
+ throw new Error(`hashBtcInputSet: invalid txid "${u.txid}"`);
6936
+ }
6937
+ if (!Number.isSafeInteger(vout) || vout < 0) {
6938
+ throw new Error(`hashBtcInputSet: invalid vout ${u.vout} for ${txid}`);
6939
+ }
6940
+ if (!Number.isSafeInteger(value) || value <= 0) {
6941
+ throw new Error(`hashBtcInputSet: invalid value ${u.value} for ${txid}:${vout}`);
6942
+ }
6943
+ return { txid, vout, value };
6944
+ });
6945
+ const seen = /* @__PURE__ */ new Set();
6946
+ for (const u of normalized) {
6947
+ const key = `${u.txid}:${u.vout}`;
6948
+ if (seen.has(key)) {
6949
+ throw new Error(`hashBtcInputSet: duplicate outpoint ${key}`);
6950
+ }
6951
+ seen.add(key);
6952
+ }
6953
+ 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("|");
6954
+ return (0, import_ethers6.keccak256)((0, import_ethers6.toUtf8Bytes)(canonical));
6955
+ }
6042
6956
  async function buildBtcExecuteEnvelope(params) {
6043
6957
  const {
6044
6958
  positionId,
@@ -6059,6 +6973,17 @@ async function buildBtcExecuteEnvelope(params) {
6059
6973
  "buildBtcExecuteEnvelope: targetAmount must be a positive number of satoshis"
6060
6974
  );
6061
6975
  }
6976
+ const utxos = params.utxos && params.utxos.length > 0 ? params.utxos : [{ txid, vout, value: satoshis }];
6977
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
6978
+ const authorizedInSet = utxos.some(
6979
+ (u) => normTxid(u.txid) === normTxid(txid) && Number(u.vout) === Number(vout)
6980
+ );
6981
+ if (!authorizedInSet) {
6982
+ throw new Error(
6983
+ `buildBtcExecuteEnvelope: the authorized UTXO ${txid}:${vout} must be part of utxos[]`
6984
+ );
6985
+ }
6986
+ const utxosHash = hashBtcInputSet(utxos);
6062
6987
  const nowSec = Date.now() / 1e3;
6063
6988
  const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
6064
6989
  const secsIntoQuantum = nowSec - currentQuantumStart;
@@ -6095,6 +7020,8 @@ async function buildBtcExecuteEnvelope(params) {
6095
7020
  // targetAddress (BTC destination)
6096
7021
  "uint256",
6097
7022
  // targetAmount (sats authorized to recipient)
7023
+ "bytes32",
7024
+ // utxosHash (canonical hash of the FULL input set — P6/#8)
6098
7025
  "bytes32"
6099
7026
  // actionHash
6100
7027
  ],
@@ -6106,6 +7033,7 @@ async function buildBtcExecuteEnvelope(params) {
6106
7033
  networkFee,
6107
7034
  targetAddress,
6108
7035
  targetAmount,
7036
+ utxosHash,
6109
7037
  actionHash
6110
7038
  ]
6111
7039
  );
@@ -6122,7 +7050,8 @@ async function buildBtcExecuteEnvelope(params) {
6122
7050
  chainId,
6123
7051
  timestamp,
6124
7052
  userSignature,
6125
- borrowerAddress
7053
+ borrowerAddress,
7054
+ utxos
6126
7055
  };
6127
7056
  }
6128
7057
 
@@ -8938,6 +9867,56 @@ var LoanQuery = class {
8938
9867
  })
8939
9868
  );
8940
9869
  }
9870
+ /**
9871
+ * Get loan-event history for every position owned by a wallet.
9872
+ *
9873
+ * Composes getUserPositions + getLoanEvents (the subgraph has no wallet-scoped
9874
+ * event collection). Each returned LoanEvents carries its own positionId, so
9875
+ * callers can flatten and merge into a single per-wallet timeline.
9876
+ *
9877
+ * Bitcoin deposits never appear in the subgraph — merge those in from an
9878
+ * Esplora source on the consumer side.
9879
+ *
9880
+ * @param userAddress - Loan-owner wallet address.
9881
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
9882
+ * @returns One LoanEvents object per position (empty array if the wallet has none).
9883
+ *
9884
+ * @example
9885
+ * ```typescript
9886
+ * const result = await loanQuery.getWalletLoanEvents('0x830b...');
9887
+ * if (result.success) {
9888
+ * const allEvents = result.value; // LoanEvents[] — one per position
9889
+ * }
9890
+ * ```
9891
+ */
9892
+ async getWalletLoanEvents(userAddress, filter) {
9893
+ if (this.config.debug) {
9894
+ log.info("\u{1F50D} Querying wallet loan events", { userAddress, filter });
9895
+ }
9896
+ return tryCatchAsync(
9897
+ async () => {
9898
+ const perPosition = await this.config.graphClient.getWalletLoanEvents(
9899
+ userAddress,
9900
+ filter
9901
+ );
9902
+ if (this.config.debug) {
9903
+ log.info("\u2705 Retrieved wallet loan events", {
9904
+ userAddress,
9905
+ positions: perPosition.length,
9906
+ totalEvents: perPosition.reduce((n, p) => n + p.totalEvents, 0)
9907
+ });
9908
+ }
9909
+ return perPosition;
9910
+ },
9911
+ (error) => new SDKError({
9912
+ message: "Failed to query wallet loan events",
9913
+ category: "SUBGRAPH" /* SUBGRAPH */,
9914
+ severity: "MEDIUM" /* MEDIUM */,
9915
+ originalError: error instanceof Error ? error : new Error(String(error)),
9916
+ context: { userAddress, filter }
9917
+ })
9918
+ );
9919
+ }
8941
9920
  /**
8942
9921
  * Clear loan cache
8943
9922
  *
@@ -11679,6 +12658,39 @@ var DiamondHandsGraph = class {
11679
12658
  totalEvents
11680
12659
  };
11681
12660
  }
12661
+ /**
12662
+ * Get the full loan-event history for every position owned by a wallet.
12663
+ *
12664
+ * The subgraph has no wallet-scoped event collection (`User` only derives
12665
+ * `positions`), so this composes the two existing wallet/position queries:
12666
+ * `getUserPositions(wallet)` to enumerate the wallet's positions, then
12667
+ * `getLoanEvents(positionId, filter)` per position. Each returned `LoanEvents`
12668
+ * already carries its own `positionId`, so callers can flatten and merge into
12669
+ * a single timeline.
12670
+ *
12671
+ * Note: Bitcoin deposits are UTXOs on the Bitcoin network and never appear in
12672
+ * the EVM subgraph — surface those from an Esplora source and merge separately.
12673
+ *
12674
+ * @param userAddress - The loan-owner wallet address.
12675
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
12676
+ * @returns One LoanEvents object per position; empty array if the wallet has none.
12677
+ */
12678
+ async getWalletLoanEvents(userAddress, filter) {
12679
+ const { positions } = await this.getUserPositions(userAddress);
12680
+ const positionIds = positions.map((p) => p?.id != null ? String(p.id) : "").filter((id) => id.length > 0);
12681
+ if (positionIds.length === 0)
12682
+ return [];
12683
+ const CONCURRENCY = 5;
12684
+ const results = [];
12685
+ for (let i = 0; i < positionIds.length; i += CONCURRENCY) {
12686
+ const batch = positionIds.slice(i, i + CONCURRENCY);
12687
+ const settled = await Promise.all(
12688
+ batch.map((positionId) => this.getLoanEvents(positionId, filter))
12689
+ );
12690
+ results.push(...settled);
12691
+ }
12692
+ return results;
12693
+ }
11682
12694
  // ============================================================================
11683
12695
  // Protocol-wide Events
11684
12696
  // ============================================================================
@@ -15091,6 +16103,15 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
15091
16103
  return {};
15092
16104
  return this.serverSession.getAuthHeader();
15093
16105
  }
16106
+ /**
16107
+ * Remove the persisted lit-ops-server session (the 24h re-mint envelope and
16108
+ * its JWT) for the current auth signer. Call on wallet disconnect or
16109
+ * explicit sign-out so the credential does not outlive the wallet
16110
+ * connection. No-op in standalone mode.
16111
+ */
16112
+ async clearServerSession() {
16113
+ await this.serverSession?.clearPersisted();
16114
+ }
15094
16115
  /**
15095
16116
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
15096
16117
  * the post-write state. We clear the entire loan-query cache (not just
@@ -15202,7 +16223,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
15202
16223
  this.serverSession = new ServerSession({
15203
16224
  signer: config.authSigner ?? config.contractSigner,
15204
16225
  serviceEndpoint: config.serviceEndpoint,
15205
- chainId: config.chainId
16226
+ chainId: config.chainId,
16227
+ persistSession: config.sessionPersistence?.enabled,
16228
+ sessionStore: config.sessionPersistence?.store,
16229
+ onSignaturePrompt: config.onSessionSignaturePrompt
15206
16230
  });
15207
16231
  }
15208
16232
  const contractManagerResult = createContractManager({
@@ -17843,9 +18867,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
17843
18867
  userSignature: auth.signature,
17844
18868
  rpcUrl: _rpcUrl || this.config.ethRpcUrl,
17845
18869
  // 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
18870
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
18871
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
18872
+ // is resolved from the on-chain BitcoinProviderRegistry and this field is server-ignored
18873
+ // — emitting it there only misleads readers into thinking it is caller-controlled. Prefer
17847
18874
  // a per-request override (mirrors the mint path's customBitcoinRpcUrl) over the config default.
17848
- ...(_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
18875
+ ...this.config.chain === "hardhat" && (_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
17849
18876
  bitcoinProviderUrl: _bitcoinRpcUrl || this.config.bitcoinProviders[0].url
17850
18877
  },
17851
18878
  ...this.config.priceProviders && {
@@ -18626,9 +19653,10 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
18626
19653
  rpcUrl: this.config.ethRpcUrl,
18627
19654
  // Pass the SDK's RPC URL to LIT Action
18628
19655
  ...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 && {
19656
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
19657
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
19658
+ // comes from the on-chain BitcoinProviderRegistry and this field is server-ignored.
19659
+ ...this.config.chain === "hardhat" && this.config.bitcoinProviders?.[0]?.url && {
18632
19660
  bitcoinProviderUrl: this.config.bitcoinProviders[0].url
18633
19661
  },
18634
19662
  ...this.config.priceProviders && {
@@ -19756,7 +20784,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19756
20784
  targetAmount: request.targetAmount,
19757
20785
  networkFee: request.networkFee,
19758
20786
  chainId,
19759
- signer
20787
+ signer,
20788
+ ...request.utxos ? { utxos: request.utxos } : {}
19760
20789
  });
19761
20790
  const headers = {
19762
20791
  "Content-Type": "application/json",
@@ -19787,7 +20816,10 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19787
20816
  userSignature: envelope.userSignature,
19788
20817
  borrowerAddress: envelope.borrowerAddress,
19789
20818
  chainId: envelope.chainId,
19790
- timestamp: envelope.timestamp
20819
+ timestamp: envelope.timestamp,
20820
+ // P6/#8: the exact input set hashed into the user signature.
20821
+ // The server must forward this set verbatim to the signer.
20822
+ utxos: envelope.utxos
19791
20823
  })
19792
20824
  }
19793
20825
  );
@@ -20127,10 +21159,13 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20127
21159
  }
20128
21160
  },
20129
21161
  rpcUrl,
20130
- ...sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
21162
+ // Hardhat-ONLY: never emitted on prod chains (server-ignored there; provider comes
21163
+ // from the on-chain BitcoinProviderRegistry). The stale-spend action reads
21164
+ // `devBitcoinProviderUrl` directly on Hardhat — this legacy alias is redundant but
21165
+ // kept behind the Hardhat gate so no prod request carries the caller-looking field.
21166
+ ...this.config.chain === "hardhat" && sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
20131
21167
  // 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.)
21168
+ // Lit Actions read `devBitcoinProviderUrl` from globalThis. Non-hardhat chains ignore it.
20134
21169
  ...sdkBitcoinProviderUrl && { devBitcoinProviderUrl: sdkBitcoinProviderUrl },
20135
21170
  ...params.minConfirmations !== void 0 && {
20136
21171
  minConfirmations: params.minConfirmations
@@ -20453,6 +21488,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20453
21488
  async getLoanEvents(positionId, filter) {
20454
21489
  return this.loanQuery.getLoanEvents(positionId, filter);
20455
21490
  }
21491
+ /**
21492
+ * Get loan-event history for every position owned by a wallet.
21493
+ * Composes getUserPositions + getLoanEvents; returns one LoanEvents per
21494
+ * position (each carrying its own positionId). Bitcoin deposits are not on the
21495
+ * subgraph — merge those in from an Esplora source on the consumer side.
21496
+ */
21497
+ async getWalletLoanEvents(userAddress, filter) {
21498
+ return this.loanQuery.getWalletLoanEvents(userAddress, filter);
21499
+ }
20456
21500
  /**
20457
21501
  * Get protocol-wide events across all indexed entity types.
20458
21502
  * Returns a merged, sorted discriminated-union timeline.
@@ -20909,6 +21953,38 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20909
21953
  }));
20910
21954
  }
20911
21955
  }
21956
+ /**
21957
+ * List a vault address's PENDING (not-yet-confirmed) incoming BTC deposits.
21958
+ *
21959
+ * A deposit is "pending" until it reaches the protocol's minimum confirmation
21960
+ * depth (6) — the confirmed balance endpoints (`getVaultBalance` /
21961
+ * `getAddressBalance`) omit anything shallower, so this surfaces in-flight
21962
+ * deposits (0–5 confirmations). Server-delegated: lit-ops-server queries its
21963
+ * configured premium Esplora provider (`/api/lit/pending-deposits`). Requires
21964
+ * service mode.
21965
+ */
21966
+ async getPendingDeposits(vaultAddress) {
21967
+ this.ensureInitialized();
21968
+ if (!isServiceModeConfig(this.config)) {
21969
+ throw new Error("getPendingDeposits requires service mode \u2014 standalone mode is not supported");
21970
+ }
21971
+ const trimmedAddress = vaultAddress?.trim();
21972
+ if (!trimmedAddress) {
21973
+ throw new Error("vaultAddress is required");
21974
+ }
21975
+ const serviceEndpoint = this.config.serviceEndpoint;
21976
+ const url = `${serviceEndpoint}/api/lit/pending-deposits?vaultAddress=${encodeURIComponent(trimmedAddress)}`;
21977
+ const response = await fetch(url, {
21978
+ headers: await this.getAuthHeader()
21979
+ });
21980
+ if (!response.ok) {
21981
+ throw new Error(`Pending deposits request failed: ${response.status} ${response.statusText}`);
21982
+ }
21983
+ const json = await response.json();
21984
+ if (!json.success)
21985
+ throw new Error(json.error ?? "Pending deposits request failed");
21986
+ return json.data ?? [];
21987
+ }
20912
21988
  // ============================================================================
20913
21989
  // HIGH-LEVEL API - Mock Token Operations (Test Networks Only)
20914
21990
  // ============================================================================
@@ -21404,6 +22480,20 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21404
22480
  if (this.config.debug) {
21405
22481
  log.info(`\u{1F310} Calling liquidation server endpoint: ${endpoint}`);
21406
22482
  }
22483
+ const signer = this.getSignerOrThrow();
22484
+ const intentSigner = await signer.getAddress();
22485
+ const chainId = Number(
22486
+ (await this.getProviderOrThrow().getNetwork()).chainId
22487
+ );
22488
+ const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
22489
+ const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
22490
+ const intentTimestamp = Math.floor(Date.now() / 1e3);
22491
+ const intentActionHash = (0, import_ethers14.keccak256)((0, import_ethers14.toUtf8Bytes)("liquidate-position"));
22492
+ const intentHash = (0, import_ethers14.solidityPackedKeccak256)(
22493
+ ["bytes32", "uint256", "uint256", "address", "bytes32"],
22494
+ [canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
22495
+ );
22496
+ const intentSignature = await signer.signMessage((0, import_ethers14.getBytes)(intentHash));
21407
22497
  const response = await fetch(endpoint, {
21408
22498
  method: "POST",
21409
22499
  headers: { "Content-Type": "application/json" },
@@ -21419,6 +22509,11 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21419
22509
  UCDController: params.contractAddresses.ucdController,
21420
22510
  LiquidationManagerModule: params.contractAddresses.liquidationManager
21421
22511
  },
22512
+ liquidatorIntent: {
22513
+ timestamp: intentTimestamp,
22514
+ chainId,
22515
+ signature: intentSignature
22516
+ },
21422
22517
  rpcUrl: params.rpcUrl || this.config.ethRpcUrl,
21423
22518
  priceProviders: this.config.priceProviders,
21424
22519
  pkpTokenId: params.pkpData.tokenId,