@gvnrdao/dh-sdk 0.0.301 → 0.0.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -93,7 +93,8 @@ var init_deployment_addresses = __esm({
93
93
  mockUsdtToken: "0xF8b7fB38b17a310960E47805Be9adba0b0e0394D",
94
94
  PositionDelegateRegistry: "0x482369De283622E7b05731875ec137d4E3D208F9",
95
95
  FeeRecipientRegistry: "0x95795F8403DDb629E8527B2934C4e71f5fC0C374",
96
- BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe"
96
+ BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe",
97
+ CarryAgentRegistry: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7"
97
98
  },
98
99
  upgraded: {
99
100
  PositionManager: {
@@ -355,7 +356,9 @@ var init_deployment_addresses = __esm({
355
356
  FEE_RECIPIENT_REGISTRY_IMPL: "0xd08513a7cB2dd354B26cDDf549616F12054c34c9",
356
357
  OPERATION_AUTHORIZATION_REGISTRY_IMPL: "0xe0D981B60351f0f8fFDA2b50C8180Fb37CeF3883",
357
358
  BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe",
358
- BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0xFF270BC4470dBA80430Bd1a3a66EB691599354fA"
359
+ BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0xFF270BC4470dBA80430Bd1a3a66EB691599354fA",
360
+ CARRY_AGENT_REGISTRY: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7",
361
+ CARRY_AGENT_REGISTRY_IMPL: "0x1b11a3D5e46F3672E643c997f0d569DFf0b64a01"
359
362
  }
360
363
  };
361
364
  SEPOLIA_CONTRACTS = {
@@ -383,7 +386,8 @@ var init_deployment_addresses = __esm({
383
386
  mockUsdtToken: "0xF8b7fB38b17a310960E47805Be9adba0b0e0394D",
384
387
  PositionDelegateRegistry: "0x482369De283622E7b05731875ec137d4E3D208F9",
385
388
  FeeRecipientRegistry: "0x95795F8403DDb629E8527B2934C4e71f5fC0C374",
386
- BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe"
389
+ BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe",
390
+ CarryAgentRegistry: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7"
387
391
  };
388
392
  MAINNET_DEPLOYMENT = {
389
393
  network: "mainnet",
@@ -418,10 +422,12 @@ var init_deployment_addresses = __esm({
418
422
  upgraded: {
419
423
  SimplePSMV2: {
420
424
  previousImplementation: "0x1a555FB8D6EDdF755a4575E541651EdB8B49a15A",
421
- newImplementation: "0x1a555FB8D6EDdF755a4575E541651EdB8B49a15A",
422
- upgradedAt: "2026-06-16T17:53:24.979Z",
423
- upgradedBy: "0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f",
424
- reason: "Initial PSM deploy / artifact sync"
425
+ newImplementation: "0x8717761444b1822c2E6916aB5bD9ac96c91b6DF5",
426
+ upgradedAt: "2026-07-06T10:41:35.000Z",
427
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x5052d1559d56b8e178699183e56c4fd84a163e987365c5af9c8d669a723c35d6 (block 25472960)",
428
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
429
+ executeTx: "0x5052d1559d56b8e178699183e56c4fd84a163e987365c5af9c8d669a723c35d6",
430
+ block: 25472960
425
431
  },
426
432
  CollateralManagerModule: {
427
433
  previousImplementation: "0x1919860e528Cd2AbbD31a17858c986b8aCc8C185",
@@ -436,6 +442,65 @@ var init_deployment_addresses = __esm({
436
442
  upgradedAt: "2026-07-08T20:33:59Z",
437
443
  upgradedBy: "AdminModule generic-op 0x2603a486c0f34554b864cc1d909b325c7e105331da10ec0fd7e0b9373fa76207 (3-of-5 ADMIN_ROLE Safes, 24h timelock; executed by 0xfb3B8cbeFB7D666A6Af34C53e189EdC31862a7be in tx 0x430c8e450f12d3d9359cde356ac5e2ef39661d7a89884b5674a596c1090f6cff)",
438
444
  reason: "Validator gas fix (#420): BitcoinAddressValidator packed-table lookups; VERSION 1.0.0 -> 1.0.1"
445
+ },
446
+ UCDController: {
447
+ newImplementation: "0x28B8cAB89D89B3F83581FD2290de2Ce84535bd0A",
448
+ upgradedBy: "Reconciled from chain \u2014 Upgraded event log unavailable; provenance omitted (never fabricated)",
449
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)"
450
+ },
451
+ PositionManagerCoreModule: {
452
+ previousImplementation: "0x6C8b0FB93F8d0239254aefaC24d4264cA2F8AAeB",
453
+ newImplementation: "0x5067Ea1d40b2Aaf4ad693bD9b6e7463C1A29E849",
454
+ upgradedAt: "2026-07-06T08:02:35.000Z",
455
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0x2002827d34A781883Ec4c83c36ECC140c3911F2B in tx 0xeb2bfe77d46394b6d0afd46beb0a80cd9beb91f6771ccb59ff9fba27d4af4f3d (block 25472167)",
456
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
457
+ executeTx: "0xeb2bfe77d46394b6d0afd46beb0a80cd9beb91f6771ccb59ff9fba27d4af4f3d",
458
+ block: 25472167
459
+ },
460
+ LiquidationManagerModule: {
461
+ previousImplementation: "0xc2f4517eeD7c4c516939a64c7938a6ab432D99c4",
462
+ newImplementation: "0xe7444C1d9652dAb31BaAdBE4C7c7e7Df3515dA46",
463
+ upgradedAt: "2026-07-06T10:40:47.000Z",
464
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0xc173b97af68cc9172ea0ddc91a2c90bf02eac1505e749d92aa044aa69891f48c (block 25472956)",
465
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
466
+ executeTx: "0xc173b97af68cc9172ea0ddc91a2c90bf02eac1505e749d92aa044aa69891f48c",
467
+ block: 25472956
468
+ },
469
+ BTCSpendAuthorizer: {
470
+ previousImplementation: "0xb003ed89F695E8819a85513fE17260b805B3c478",
471
+ newImplementation: "0x40c9e8C46C65E0A57BB63da5368868A9E8Df81A5",
472
+ upgradedAt: "2026-07-06T10:40:59.000Z",
473
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0xe1b1884a7e76c7fd569522545a04e49d5128d83d27b3892cda59f143b53b5281 (block 25472957)",
474
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
475
+ executeTx: "0xe1b1884a7e76c7fd569522545a04e49d5128d83d27b3892cda59f143b53b5281",
476
+ block: 25472957
477
+ },
478
+ PositionManager: {
479
+ previousImplementation: "0x9f52BaBB40FbFC5863e45B1A4014cB433950c684",
480
+ newImplementation: "0x600Dc0D1C2c0F94882B6Bc09172f8650047Ad5B1",
481
+ upgradedAt: "2026-07-06T08:42:47.000Z",
482
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0x2002827d34A781883Ec4c83c36ECC140c3911F2B in tx 0xa1fbe31cb71736514a868e681573dff22c9fab91d69cec157a24a30e29eadbe6 (block 25472368)",
483
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
484
+ executeTx: "0xa1fbe31cb71736514a868e681573dff22c9fab91d69cec157a24a30e29eadbe6",
485
+ block: 25472368
486
+ },
487
+ LoanOperationsManagerModule: {
488
+ previousImplementation: "0x56671D671e492Dae625413d463D47605Cf54edf6",
489
+ newImplementation: "0x7d90aAB4466f59BA9aC9C7a9BF14dC53734336e7",
490
+ upgradedAt: "2026-07-06T10:41:23.000Z",
491
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x327ae9f7a3917788d22dfe95611e1afcd18583f210b03a119d9f04eca50b9e7a (block 25472959)",
492
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
493
+ executeTx: "0x327ae9f7a3917788d22dfe95611e1afcd18583f210b03a119d9f04eca50b9e7a",
494
+ block: 25472959
495
+ },
496
+ OperationAuthorizationRegistry: {
497
+ previousImplementation: "0x327662917Ac8EEda47851d74AF6A151348636fCe",
498
+ newImplementation: "0x493118746840EDB2D396E71632D2E774CA14070e",
499
+ upgradedAt: "2026-07-06T10:41:47.000Z",
500
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x9820e259700865a281b8376e248044048ba2fed1d17c811ac66ac54d36711a5b (block 25472961)",
501
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
502
+ executeTx: "0x9820e259700865a281b8376e248044048ba2fed1d17c811ac66ac54d36711a5b",
503
+ block: 25472961
439
504
  }
440
505
  },
441
506
  latestEnv: {
@@ -448,9 +513,37 @@ var init_deployment_addresses = __esm({
448
513
  BITCOIN_PROVIDER_REGISTRY: "0x4ab3Fb96e02893Ed6FAa61bad1918390c8DbbFFC",
449
514
  BITCOIN_PROVIDER_REGISTRY_IMPL: "0x7Bc9473E45fb3EA594e8b13992F57D0444902ddB",
450
515
  BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY: "0xf667f77FDba7458345f227eB6Bfe4148243E7270",
451
- BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0x11731bFEC3068F2cc19D016381dfEA3B93013b1C",
516
+ BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0x190c08300Ae650fb88ed8156445BBeb13bE9E8a3",
452
517
  SIMPLE_PSM_V2: "0xfE0C14B4E614f40Bf58845b4B04CeD17fd116532",
453
- SIMPLE_PSM_V2_IMPL: "0x1a555FB8D6EDdF755a4575E541651EdB8B49a15A"
518
+ SIMPLE_PSM_V2_IMPL: "0x8717761444b1822c2E6916aB5bD9ac96c91b6DF5",
519
+ UCD_CONTROLLER: "0x4b4f8063048bCD59D469219E3f07C6aaDF4f6157",
520
+ UCD_CONTROLLER_IMPL: "0x28B8cAB89D89B3F83581FD2290de2Ce84535bd0A",
521
+ UCD_TOKEN: "0xaAaB6B05E5EecB015898483d4e6703d7F218850b",
522
+ UCD_TOKEN_IMPL: "0x5f09C660036021D7e916DF47234C24a2536C3A72",
523
+ POSITION_MANAGER_CORE_MODULE: "0xd895B685914c1BE4e1c7aab9f89a90c203DE8d22",
524
+ POSITION_MANAGER_CORE_MODULE_IMPL: "0x5067Ea1d40b2Aaf4ad693bD9b6e7463C1A29E849",
525
+ TERM_MANAGER_MODULE: "0xDB99CdF883ffa38acD099bC39F8d1f4508c9239E",
526
+ TERM_MANAGER_MODULE_IMPL: "0x971f63f92990e59270E971612dd399fF2C63B0c3",
527
+ COLLATERAL_MANAGER_MODULE: "0x2Eb74FEc478218f5C260aAc78Bb8eC297F9d7135",
528
+ COLLATERAL_MANAGER_MODULE_IMPL: "0x55110B9dd6B2c9f812564e7a1ef42b3c76E1A3Bb",
529
+ LIQUIDATION_MANAGER_MODULE: "0x74B28695352E7bf736D6c9F76ccb7B4439477De9",
530
+ LIQUIDATION_MANAGER_MODULE_IMPL: "0xe7444C1d9652dAb31BaAdBE4C7c7e7Df3515dA46",
531
+ CIRCUIT_BREAKER_MODULE: "0xF7b545d3a4Ee056D06909AB367E2F7bFFE3cB1BC",
532
+ CIRCUIT_BREAKER_MODULE_IMPL: "0x40e8328D61342C6ADC51dfdC723296c908f95038",
533
+ ADMIN_MODULE: "0x0023B71B586795eE5fa4E83E384345C1EC8Bb946",
534
+ ADMIN_MODULE_IMPL: "0xb4C5C7E051A1e672F014B25CB5D55B225Aa0D512",
535
+ BTC_SPEND_AUTHORIZER: "0xfBbF108486fa07F1B06a811C5b66ed68DD9c508a",
536
+ BTC_SPEND_AUTHORIZER_IMPL: "0x40c9e8C46C65E0A57BB63da5368868A9E8Df81A5",
537
+ POSITION_MANAGER_VIEWS: "0x9225647a30bbACA4Acc0491B786869036914242e",
538
+ POSITION_MANAGER_VIEWS_IMPL: "0x9225647a30bbACA4Acc0491B786869036914242e",
539
+ POSITION_MANAGER: "0x12A1836e56139ccaD8722B89519d84233efc224B",
540
+ POSITION_MANAGER_IMPL: "0x600Dc0D1C2c0F94882B6Bc09172f8650047Ad5B1",
541
+ LOAN_OPERATIONS_MANAGER_MODULE: "0x2561974A40F1AEed1251d0A090A83cF8536FB9E9",
542
+ LOAN_OPERATIONS_MANAGER_MODULE_IMPL: "0x7d90aAB4466f59BA9aC9C7a9BF14dC53734336e7",
543
+ UPGRADE_VALIDATOR: "0xC46Ae71F22e3741A6CF46E399aE914C4868fEbed",
544
+ UPGRADE_VALIDATOR_IMPL: "0x4f835e77D1211299a1cbb8Aa2b42d9B390cC95CA",
545
+ OPERATION_AUTHORIZATION_REGISTRY: "0xD4505645b724754f5170a2027804a4265e860d44",
546
+ OPERATION_AUTHORIZATION_REGISTRY_IMPL: "0x493118746840EDB2D396E71632D2E774CA14070e"
454
547
  }
455
548
  };
456
549
  MAINNET_CONTRACTS = {
@@ -481,44 +574,44 @@ var init_deployment_addresses = __esm({
481
574
  LOCALHOST_DEPLOYMENT = {
482
575
  network: "localhost",
483
576
  chainId: 31337,
484
- timestamp: "2026-03-23T15:33:34.068Z",
577
+ timestamp: "2026-06-11T12:30:59.589Z",
485
578
  deployer: "",
486
579
  contracts: {
487
- MessageHashBuilder: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
488
- UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
580
+ MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
581
+ UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
489
582
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
490
583
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
491
- PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
492
- TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
493
- LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
584
+ PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
585
+ TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
586
+ LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
494
587
  BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
495
588
  CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
496
589
  LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
497
- CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
590
+ CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
498
591
  AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
499
- PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
500
- PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB",
501
- OperationAuthorizationRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0",
502
- PKPValidation: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
592
+ PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
593
+ PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
594
+ OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
595
+ PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
503
596
  }
504
597
  };
505
598
  LOCALHOST_CONTRACTS = {
506
- MessageHashBuilder: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
507
- UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
599
+ MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
600
+ UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
508
601
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
509
602
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
510
- PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
511
- TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
512
- LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
603
+ PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
604
+ TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
605
+ LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
513
606
  BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
514
607
  CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
515
608
  LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
516
- CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
609
+ CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
517
610
  AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
518
- PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
519
- PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB",
520
- OperationAuthorizationRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0",
521
- PKPValidation: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
611
+ PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
612
+ PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
613
+ OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
614
+ PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
522
615
  };
523
616
  ALL_DEPLOYMENTS = {
524
617
  sepolia: SEPOLIA_DEPLOYMENT,
@@ -1210,30 +1303,30 @@ var require_pkg_src = __commonJS({
1210
1303
  }
1211
1304
  },
1212
1305
  btcTransactionSigner: {
1213
- cid: "QmcPG9C5Yx9JGrBKCfSVWbKuiRXn8XFwb91yNxQqY4VSJD",
1306
+ cid: "QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk",
1214
1307
  authorizedCidHex: cidToHex(
1215
- "QmcPG9C5Yx9JGrBKCfSVWbKuiRXn8XFwb91yNxQqY4VSJD"
1308
+ "QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk"
1216
1309
  ),
1217
1310
  name: "Btc Transaction Signer",
1218
1311
  description: "Production Btc Transaction Signer",
1219
- version: "1.0.1",
1312
+ version: "1.1.1",
1220
1313
  deployed: true,
1221
- deployedAt: 1783451891948,
1222
- size: 43688,
1223
- hash: "1ea0f0ac66cc68fc9b098dabb3c5e3ba96c25f13b133b95a06eac477028fdb1a"
1314
+ deployedAt: 1784316496778,
1315
+ size: 63679,
1316
+ hash: "b12aa725797af5ef07f8e6945f05741f583d6e479a4670b1121e97c58fe63191"
1224
1317
  },
1225
1318
  ucdMintValidator: {
1226
- cid: "QmPir8F8jmJ4ssQpEC3xFxkEnZxMHcgS6VxBQE1jkTLj5K",
1319
+ cid: "QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW",
1227
1320
  authorizedCidHex: cidToHex(
1228
- "QmPir8F8jmJ4ssQpEC3xFxkEnZxMHcgS6VxBQE1jkTLj5K"
1321
+ "QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW"
1229
1322
  ),
1230
1323
  name: "Ucd Mint Validator",
1231
1324
  description: "Production Ucd Mint Validator",
1232
- version: "1.0.0",
1325
+ version: "1.2.1",
1233
1326
  deployed: true,
1234
- deployedAt: 1781542207366,
1235
- size: 72074,
1236
- hash: "567a51d3beaedbda4f0af2636835ca8a2af89c2264e91f8dc31f522c1a424b7b",
1327
+ deployedAt: 1784316505431,
1328
+ size: 80381,
1329
+ hash: "132a0a8e6e9a1b85d02e00c0e57eaa371d825717255a28c9e644e4b521760026",
1237
1330
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1238
1331
  pkp: {
1239
1332
  publicKey: "0x041ab17cd91fc5c0b761eea6092d032807561b621b82c488826776e04a9158d61ba64d809a4729f0501289d980732cb06d8dfd06999dd9c8efd2f495dad78b31bb",
@@ -1241,17 +1334,17 @@ var require_pkg_src = __commonJS({
1241
1334
  }
1242
1335
  },
1243
1336
  processPaymentValidator: {
1244
- cid: "QmVq19NXJEpMMvaWQdsU5czLoqh28WGWoGgKMSYFaCmfa9",
1337
+ cid: "QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ",
1245
1338
  authorizedCidHex: cidToHex(
1246
- "QmVq19NXJEpMMvaWQdsU5czLoqh28WGWoGgKMSYFaCmfa9"
1339
+ "QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ"
1247
1340
  ),
1248
1341
  name: "Process Payment Validator",
1249
1342
  description: "Production Process Payment Validator",
1250
- version: "1.0.0",
1343
+ version: "1.2.1",
1251
1344
  deployed: true,
1252
- deployedAt: 1781542209526,
1253
- size: 68239,
1254
- hash: "ada27fea57aa3f3b861c49e75ed1f80b72500fdccc1d6e88ee1a3c1396b81867",
1345
+ deployedAt: 1784316520125,
1346
+ size: 76534,
1347
+ hash: "dbdf43d3f1636e0ee883d349ca28f24c5c38ed7e062b76ca75498aaf5fe594ea",
1255
1348
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1256
1349
  pkp: {
1257
1350
  publicKey: "0x04cfccb9b6c7addc79469c0e9fafebef4c70801302c62cdb26b610723a9b8527d23f98a40158c7340d83fd0e62d02b5ec74c12f7a86d98e8c6973b4811110f19ad",
@@ -1259,17 +1352,17 @@ var require_pkg_src = __commonJS({
1259
1352
  }
1260
1353
  },
1261
1354
  extendPositionValidator: {
1262
- cid: "QmP2u3bgKLtfqsFMp6Li72zMY8ZZH3ttAqp9W2wwonUmcb",
1355
+ cid: "QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4",
1263
1356
  authorizedCidHex: cidToHex(
1264
- "QmP2u3bgKLtfqsFMp6Li72zMY8ZZH3ttAqp9W2wwonUmcb"
1357
+ "QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4"
1265
1358
  ),
1266
1359
  name: "Extend Position Validator",
1267
1360
  description: "Production Extend Position Validator",
1268
- version: "1.0.1",
1361
+ version: "1.2.1",
1269
1362
  deployed: true,
1270
- deployedAt: 1781542211640,
1271
- size: 66543,
1272
- hash: "0db666fa2888e9acea13d8e4d79edd4ca252f5a4bb06d97e53aa14a9a34b7143",
1363
+ deployedAt: 1784316534278,
1364
+ size: 74352,
1365
+ hash: "c17bfc9abef16f2372316a14c14281e2acc98afdd1662d095360f1d1c5383f1d",
1273
1366
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1274
1367
  pkp: {
1275
1368
  publicKey: "0x04e52338f6a0c3362800f1a94c4c8f08be5030dde0fa217388c53d1c1a4790f4443c9f2886b2d2d8f4b75a9a156e9b028011b1a1cf7648da0e556aa980ec692836",
@@ -1277,17 +1370,17 @@ var require_pkg_src = __commonJS({
1277
1370
  }
1278
1371
  },
1279
1372
  btcWithdrawal: {
1280
- cid: "QmXtmLdGuEqMKKAtV7d8sHpUV32Kt74na4dRQdpWLasbHJ",
1373
+ cid: "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2",
1281
1374
  authorizedCidHex: cidToHex(
1282
- "QmXtmLdGuEqMKKAtV7d8sHpUV32Kt74na4dRQdpWLasbHJ"
1375
+ "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2"
1283
1376
  ),
1284
1377
  name: "Btc Withdrawal",
1285
1378
  description: "Production Btc Withdrawal",
1286
- version: "1.0.0",
1379
+ version: "1.2.1",
1287
1380
  deployed: true,
1288
- deployedAt: 1781542212709,
1289
- size: 79782,
1290
- hash: "e824eb473df8eba3a025ac3adc46730dbf89f90884d7c05a78cc07b90d6bc4df",
1381
+ deployedAt: 1784316549409,
1382
+ size: 90692,
1383
+ hash: "ca7276763142fe1fc812b70f6319973000faf338a91c531f898f8f6406035c64",
1291
1384
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1292
1385
  pkp: {
1293
1386
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1295,17 +1388,17 @@ var require_pkg_src = __commonJS({
1295
1388
  }
1296
1389
  },
1297
1390
  liquidationValidator: {
1298
- cid: "QmVicBXuZVY8t4DXLPjvXjzLqnBtnogAh5HT7BaL9VeDwF",
1391
+ cid: "QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx",
1299
1392
  authorizedCidHex: cidToHex(
1300
- "QmVicBXuZVY8t4DXLPjvXjzLqnBtnogAh5HT7BaL9VeDwF"
1393
+ "QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx"
1301
1394
  ),
1302
1395
  name: "Liquidation Validator",
1303
1396
  description: "Production Liquidation Validator",
1304
- version: "1.0.0",
1397
+ version: "1.2.1",
1305
1398
  deployed: true,
1306
- deployedAt: 1781542213421,
1307
- size: 58444,
1308
- hash: "0f42e9f8984433fef95875d35eb3c8d985e7ce15ad309e6aa1093d6fd1ba0bb5",
1399
+ deployedAt: 1784316560010,
1400
+ size: 66523,
1401
+ hash: "f74d0163c47d7db04d1547f72c3a9bc1e5b499669089cc6464479a361462ce81",
1309
1402
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1310
1403
  pkp: {
1311
1404
  publicKey: "0x041a241de58976c13e3d7ca5a18e494f7330151e3818706135d50176e78ffb673e58b11f0b67a87c73fade6c6b27d6e7a7377ea9f4c10bf31880f1d725f18cc6f8",
@@ -1331,17 +1424,17 @@ var require_pkg_src = __commonJS({
1331
1424
  }
1332
1425
  },
1333
1426
  loanVaultBtcBalance: {
1334
- cid: "QmUmqL4LcjDSpw2TtuWMcw8xYk8hPxgagY2qHDU8Mx1PLG",
1427
+ cid: "QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu",
1335
1428
  authorizedCidHex: cidToHex(
1336
- "QmUmqL4LcjDSpw2TtuWMcw8xYk8hPxgagY2qHDU8Mx1PLG"
1429
+ "QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu"
1337
1430
  ),
1338
1431
  name: "Loan Vault Btc Balance",
1339
1432
  description: "Production Loan Vault Btc Balance",
1340
- version: "1.0.0",
1433
+ version: "1.2.1",
1341
1434
  deployed: true,
1342
- deployedAt: 1781542215432,
1343
- size: 48397,
1344
- hash: "da3750a70e532b87f3590a3e2d8c2d51e700589f6e111bb291384ab01304af35",
1435
+ deployedAt: 1784316574669,
1436
+ size: 56477,
1437
+ hash: "917535f2911055b2ade2204fd868dd8b7294c7ade23e82aea488aeb0872e35d7",
1345
1438
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1346
1439
  pkp: {
1347
1440
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1355,17 +1448,17 @@ var require_pkg_src = __commonJS({
1355
1448
  // Requires BitcoinProviderRegistry.addAllowedCid(chainId, keccak256(utf8(cid)))
1356
1449
  // on each target chain (the action decrypts registry slots). No OAR entry.
1357
1450
  btcAddressBalance: {
1358
- cid: "QmWRDAg7ZCBFFhGfdXR7aUtQsG5qqBb1yVdLCxh8dPD7G5",
1451
+ cid: "QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA",
1359
1452
  authorizedCidHex: cidToHex(
1360
- "QmWRDAg7ZCBFFhGfdXR7aUtQsG5qqBb1yVdLCxh8dPD7G5"
1453
+ "QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA"
1361
1454
  ),
1362
1455
  name: "Btc Address Balance",
1363
1456
  description: "Address-only signed total BTC balance via registry consensus (no positionId, no price)",
1364
- version: "0.1.0",
1457
+ version: "0.2.1",
1365
1458
  deployed: true,
1366
- deployedAt: 1781542217452,
1367
- size: 25519,
1368
- hash: "000e2f8bdbccdc95b0db0fb3f12412e2e76eea16a8456d37914d324437ae291c",
1459
+ deployedAt: 1784316590403,
1460
+ size: 26986,
1461
+ hash: "49fb14265f3208916f12806df04492900bd08fb1b14d9651ee6278cd9ae4a428",
1369
1462
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1370
1463
  pkp: {
1371
1464
  publicKey: "0x0498cbb31facfa26e3ec83ccdabfe4fe341ca0282e0c3cb81da7113723b9726e1e49efa92165cc3faa66ced4454cea56dbab12267852d6525a55d7c4f128b912cd",
@@ -1399,17 +1492,17 @@ var require_pkg_src = __commonJS({
1399
1492
  hash: "03ee5be239c91c735d2c95db89a0d1b77e602cfc18af8eb64b3281fddcd90a72"
1400
1493
  },
1401
1494
  staleSpendRecoveryValidator: {
1402
- cid: "QmWqJZqoKUqMaGTXe9WUSwdDyeBwXJ9dnR3ioFnVK4RkN5",
1495
+ cid: "QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK",
1403
1496
  authorizedCidHex: cidToHex(
1404
- "QmWqJZqoKUqMaGTXe9WUSwdDyeBwXJ9dnR3ioFnVK4RkN5"
1497
+ "QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK"
1405
1498
  ),
1406
1499
  name: "Stale Spend Recovery Validator",
1407
1500
  description: "Production Stale Spend Recovery Validator",
1408
- version: "1.0.0",
1501
+ version: "1.1.2",
1409
1502
  deployed: true,
1410
- deployedAt: 1781542219438,
1411
- size: 34413,
1412
- hash: "26b11730cf5d9eb4af0b1fe65192d26ccee50369c0472211a0ea11e26ef51cc5",
1503
+ deployedAt: 1784316604502,
1504
+ size: 36785,
1505
+ hash: "28bfe44970541fefe3bae8bd54f25e8775d7cc61b5832a40ba1c18587b6b6358",
1413
1506
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1414
1507
  pkp: {
1415
1508
  publicKey: "0x046da0ad6da7dd4a0063cbeac6f2bdb6a9889c2178b23470833356a8ee5630f7f0eee0db348d9d9aaaadd23eccf0063c8f68ee5e446abe30b882bf1de4c932bc21",
@@ -2197,6 +2290,20 @@ ${errorReport}`);
2197
2290
  /* REGTEST */
2198
2291
  // Local development
2199
2292
  };
2293
+ var CHAIN_TO_CHAIN_ID = {
2294
+ [
2295
+ "ethereum"
2296
+ /* ETHEREUM */
2297
+ ]: 1,
2298
+ [
2299
+ "sepolia"
2300
+ /* SEPOLIA */
2301
+ ]: 11155111,
2302
+ [
2303
+ "hardhat"
2304
+ /* HARDHAT */
2305
+ ]: 1337
2306
+ };
2200
2307
  function validateChain(chain) {
2201
2308
  const normalized = chain.toLowerCase().trim();
2202
2309
  if (normalized === "sepolia") {
@@ -2493,6 +2600,27 @@ ${errorReport}`);
2493
2600
  function createProtocolParametersModule(config) {
2494
2601
  return new ProtocolParametersModule(config);
2495
2602
  }
2603
+ function parseVaultAddress(rawVaultAddress, chain) {
2604
+ try {
2605
+ const parsed = JSON.parse(rawVaultAddress);
2606
+ if (typeof parsed === "object" && parsed !== null) {
2607
+ const chainLower = chain.toLowerCase();
2608
+ if (chainLower === "sepolia") {
2609
+ return parsed.regtest || parsed.testnet || parsed.mainnet;
2610
+ }
2611
+ const bitcoinNetwork = getBitcoinNetworkForChain(chain);
2612
+ if (bitcoinNetwork === "regtest") {
2613
+ return parsed.regtest || parsed.testnet || parsed.mainnet;
2614
+ }
2615
+ if (bitcoinNetwork === "testnet") {
2616
+ return parsed.testnet || parsed.regtest || parsed.mainnet;
2617
+ }
2618
+ return parsed.mainnet;
2619
+ }
2620
+ } catch (e) {
2621
+ }
2622
+ return rawVaultAddress;
2623
+ }
2496
2624
  var VaultSnapshotModule = class {
2497
2625
  constructor(config) {
2498
2626
  this.config = config;
@@ -2600,112 +2728,11 @@ ${errorReport}`);
2600
2728
  timestamp: Date.now()
2601
2729
  };
2602
2730
  }
2603
- /**
2604
- * Fast vault snapshot using balance method only (no UTXO validation)
2605
- * This is optimized for speed and should be used when UTXO details are not needed.
2606
- *
2607
- * WARNING: This method skips UTXO validation. Use ONLY for non-critical operations.
2608
- * For authorization/security operations, ALWAYS use getVaultSnapshot().
2609
- *
2610
- * @param positionId - Position to snapshot
2611
- * @returns Complete VaultSnapshot with all metrics (using fast balance method)
2612
- */
2613
- async getVaultSnapshotFast(positionId) {
2614
- const positionState = await this.queryPositionState(positionId);
2615
- console.log(
2616
- `[Vault Snapshot] Raw vault address from contract: "${positionState.vaultAddress}"`
2617
- );
2618
- const useStubbedBtcData = this.config.debugOverrides?.useStubbedBtcData === true;
2619
- const [balanceResult, btcPriceUsd] = useStubbedBtcData ? this.buildStubbedBtcSnapshotInputs(positionState) : await Promise.all([
2620
- this.config.vaultBalance.calculateTrustedBalance(
2621
- positionId,
2622
- positionState.vaultAddress,
2623
- true
2624
- // Always skip UTXO validation for fast path
2625
- ),
2626
- this.config.priceOracle.getBTCPrice()
2627
- ]);
2628
- const protocolParams = createProtocolParametersModule({
2629
- termManagerAddress: this.config.termManagerAddress,
2630
- loanOpsManagerAddress: this.config.loanOpsManagerAddress,
2631
- liquidationManagerAddress: this.config.liquidationManagerAddress,
2632
- chain: this.config.chain,
2633
- chainId: this.config.chainId || 1,
2634
- // Default to mainnet if not provided
2635
- rpcUrl: this.config.rpcUrl
2636
- });
2637
- const termForFees = this.config.termOverride ?? positionState.selectedTerm;
2638
- const [liquidationThresholdBps, maxEscalatedThresholdBps, termFees] = await Promise.all([
2639
- protocolParams.getLiquidationThreshold(),
2640
- protocolParams.getMaxEscalatedThreshold(),
2641
- protocolParams.getTermFees(termForFees)
2642
- ]);
2643
- const termStatus = calculateTermStatus(
2644
- positionState.termStartTimestamp,
2645
- positionState.selectedTerm
2646
- );
2647
- const currentLiquidationThreshold = calculateLiquidationThreshold(
2648
- termStatus.isExpired,
2649
- termStatus.daysIntoGracePeriod,
2650
- liquidationThresholdBps,
2651
- maxEscalatedThresholdBps
2652
- );
2653
- const collateralMetrics = calculateCollateralMetrics(
2654
- balanceResult.availableBalance,
2655
- btcPriceUsd,
2656
- positionState.ucdDebt
2657
- );
2658
- const isLiquidatable = collateralMetrics.collateralRatioBps < currentLiquidationThreshold;
2659
- const marginToLiquidationBps = collateralMetrics.collateralRatioBps - currentLiquidationThreshold;
2660
- return {
2661
- // Raw state
2662
- positionId: positionState.positionId,
2663
- pkpId: positionState.pkpId,
2664
- borrower: positionState.borrower,
2665
- vaultAddress: positionState.vaultAddress,
2666
- ucdDebt: positionState.ucdDebt,
2667
- termStartTimestamp: positionState.termStartTimestamp,
2668
- selectedTerm: positionState.selectedTerm,
2669
- status: positionState.status,
2670
- // Loan status enum value (0-7)
2671
- expiryAt: positionState.expiryAt,
2672
- // Loan expiry timestamp
2673
- previousExpiryAt: positionState.previousExpiryAt,
2674
- // expiryAt before last renewal; 0 if never renewed
2675
- totalTerm: positionState.totalTerm,
2676
- // total accumulated loan months; 0 before first renewal
2677
- lastRenewedAt: positionState.lastRenewedAt,
2678
- // timestamp of most recent renewal; createdAt at origination; 0 for pre-H-5 positions
2679
- // Bitcoin state
2680
- totalBTCSats: balanceResult.totalBalance,
2681
- totalUTXOs: balanceResult.totalUTXOs,
2682
- authorizedSpendsSats: balanceResult.authorizedBalance,
2683
- authorizedSpendsHash: balanceResult.authorizedSpendsHash,
2684
- availableBTCSats: balanceResult.availableBalance,
2685
- availableUTXOs: balanceResult.availableUTXOs,
2686
- // Price data
2687
- btcPriceUsd,
2688
- // Calculated metrics
2689
- collateralValueUsd: collateralMetrics.collateralValueUsd,
2690
- collateralRatioBps: collateralMetrics.collateralRatioBps,
2691
- // Term status
2692
- termDurationDays: termStatus.termDurationDays,
2693
- termLengthDays: termStatus.termLengthDays,
2694
- isExpired: termStatus.isExpired,
2695
- daysUntilExpiry: termStatus.daysUntilExpiry,
2696
- daysIntoGracePeriod: termStatus.daysIntoGracePeriod,
2697
- // Liquidation risk
2698
- currentLiquidationThreshold,
2699
- isLiquidatable,
2700
- marginToLiquidationBps,
2701
- // Protocol parameters (from smart contracts)
2702
- liquidationThresholdBps,
2703
- originationFeeBps: termFees.originationFeeBps,
2704
- extensionFeeBps: termFees.extensionFeeBps,
2705
- // Metadata
2706
- timestamp: Date.now()
2707
- };
2708
- }
2731
+ // NOTE (remediation P1/#5): `getVaultSnapshotFast` — a snapshot built on the
2732
+ // skip-UTXO-validation balance path was deleted. It had zero callers, and
2733
+ // a snapshot whose balance skipped UTXO validation + consensus must never
2734
+ // exist within reach of a signing flow. Every snapshot now runs the full
2735
+ // validation path above.
2709
2736
  buildStubbedBtcSnapshotInputs(positionState) {
2710
2737
  const stubbedAvailableBtcSats = BigInt(
2711
2738
  this.config.debugOverrides?.stubbedAvailableBtcSats ?? "10000000"
@@ -2834,14 +2861,20 @@ ${errorReport}`);
2834
2861
  expiryAt,
2835
2862
  selectedTermMonths
2836
2863
  );
2864
+ const parsedVaultAddress = parseVaultAddress(
2865
+ position.vaultAddress,
2866
+ validateChain(this.config.chain)
2867
+ );
2868
+ if (typeof parsedVaultAddress !== "string" || parsedVaultAddress.length === 0) {
2869
+ throw new Error(
2870
+ `Position ${positionIdBytes32} vaultAddress has no entry for chain "${this.config.chain}"`
2871
+ );
2872
+ }
2837
2873
  return {
2838
2874
  positionId: position.positionId,
2839
2875
  pkpId: position.pkpId,
2840
2876
  borrower: position.borrower,
2841
- vaultAddress: this.parseVaultAddress(
2842
- position.vaultAddress,
2843
- validateChain(this.config.chain)
2844
- ),
2877
+ vaultAddress: parsedVaultAddress,
2845
2878
  ucdDebt: BigInt(position.ucdDebt.toString()),
2846
2879
  termStartTimestamp,
2847
2880
  selectedTerm: selectedTermMonths,
@@ -2852,36 +2885,8 @@ ${errorReport}`);
2852
2885
  lastRenewedAt
2853
2886
  };
2854
2887
  }
2855
- /**
2856
- * Parse vault address based on format and network
2857
- *
2858
- * Supports two formats:
2859
- * 1. Single string: "mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"
2860
- * 2. JSON object: {"mainnet":"bc1q...","testnet":"mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"}
2861
- *
2862
- * @param rawVaultAddress Raw vault address from contract
2863
- * @param chain EVM chain (sepolia, ethereum)
2864
- * @returns Parsed Bitcoin address for the appropriate network
2865
- */
2866
- parseVaultAddress(rawVaultAddress, chain) {
2867
- try {
2868
- const parsed = JSON.parse(rawVaultAddress);
2869
- if (typeof parsed === "object" && parsed !== null) {
2870
- const chainLower = chain.toLowerCase();
2871
- if (chainLower === "sepolia") {
2872
- return parsed.regtest || parsed.testnet || parsed.mainnet;
2873
- }
2874
- const bitcoinNetwork = getBitcoinNetworkForChain(chain);
2875
- if (bitcoinNetwork === "testnet") {
2876
- return parsed.testnet || parsed.regtest || parsed.mainnet;
2877
- } else {
2878
- return parsed.mainnet;
2879
- }
2880
- }
2881
- } catch (e) {
2882
- }
2883
- return rawVaultAddress;
2884
- }
2888
+ // parseVaultAddress moved to `modules/vault-address.ts` (shared with the
2889
+ // phase-2 signer for the P6/#7b vault-identity binding).
2885
2890
  /**
2886
2891
  * Quick check if position is liquidatable
2887
2892
  *
@@ -3219,6 +3224,13 @@ ${auth.clientId}`;
3219
3224
  return {};
3220
3225
  }
3221
3226
  }
3227
+ function assertSaneSatoshis(value, context) {
3228
+ if (!Number.isSafeInteger(value) || value < 0 || value > MAX_BTC_AMOUNT) {
3229
+ throw new Error(
3230
+ `${context}: satoshi value ${value} is not a sane amount (must be an integer in [0, ${MAX_BTC_AMOUNT}])`
3231
+ );
3232
+ }
3233
+ }
3222
3234
  var BitcoinDataProvider = class {
3223
3235
  constructor(config) {
3224
3236
  this.config = config;
@@ -3365,13 +3377,16 @@ ${auth.clientId}`;
3365
3377
  address
3366
3378
  );
3367
3379
  const utxos = unspentOutputs.map(
3368
- (utxo) => ({
3369
- txid: utxo.txid,
3370
- vout: utxo.vout,
3371
- satoshis: BigInt(Math.round(utxo.amount * 1e8)),
3372
- // BTC to satoshis as bigint
3373
- confirmations: utxo.confirmations
3374
- })
3380
+ (utxo) => {
3381
+ const satNumber = Math.round(utxo.amount * 1e8);
3382
+ assertSaneSatoshis(satNumber, `fetchUTXOsFromRPC ${utxo.txid}:${utxo.vout}`);
3383
+ return {
3384
+ txid: utxo.txid,
3385
+ vout: utxo.vout,
3386
+ satoshis: BigInt(satNumber),
3387
+ confirmations: utxo.confirmations
3388
+ };
3389
+ }
3375
3390
  );
3376
3391
  return utxos;
3377
3392
  }
@@ -3413,6 +3428,7 @@ ${auth.clientId}`;
3413
3428
  "Invalid UTXO row: missing txid, vout/n, or value/satoshis"
3414
3429
  );
3415
3430
  }
3431
+ assertSaneSatoshis(satRaw, `parseBlockstreamUTXOs ${txid}:${vout}`);
3416
3432
  utxos.push({
3417
3433
  txid,
3418
3434
  vout,
@@ -3591,7 +3607,10 @@ ${auth.clientId}`;
3591
3607
  `Invalid response: missing required fields. funded_txo_sum: ${fundedTxoSum}, spent_txo_sum: ${spentTxoSum}`
3592
3608
  );
3593
3609
  }
3610
+ assertSaneSatoshis(fundedTxoSum, `getBalance ${cleanAddress} funded_txo_sum`);
3611
+ assertSaneSatoshis(spentTxoSum, `getBalance ${cleanAddress} spent_txo_sum`);
3594
3612
  const confirmedBalance = fundedTxoSum - spentTxoSum;
3613
+ assertSaneSatoshis(confirmedBalance, `getBalance ${cleanAddress} confirmed balance`);
3595
3614
  const balance = BigInt(confirmedBalance);
3596
3615
  this.logger.log("3", `Balance: ${balance.toString()} sats`);
3597
3616
  this.logger.log(
@@ -4070,20 +4089,590 @@ ${auth.clientId}`;
4070
4089
  return priceWith8Decimals;
4071
4090
  }
4072
4091
  };
4092
+ var MAX_CLASSIFIED_PARENTS = 5;
4093
+ var CLASSIFICATION_CONCURRENCY = 2;
4094
+ var CONFIRMATION_SKEW_TOLERANCE = 3;
4095
+ async function mapWithConcurrency(items, limit, fn) {
4096
+ const results = new Array(items.length);
4097
+ let next = 0;
4098
+ const workers = Array.from(
4099
+ { length: Math.min(limit, items.length) },
4100
+ async () => {
4101
+ while (true) {
4102
+ const i = next++;
4103
+ if (i >= items.length) {
4104
+ return;
4105
+ }
4106
+ results[i] = await fn(items[i]);
4107
+ }
4108
+ }
4109
+ );
4110
+ await Promise.all(workers);
4111
+ return results;
4112
+ }
4113
+ function findOutputClaimMismatch(claimedUtxos, outputs, cleanVaultAddress) {
4114
+ for (const claim of claimedUtxos) {
4115
+ const output = outputs.find((o) => o.index === claim.vout);
4116
+ if (!output) {
4117
+ return `claimed output ${claim.txid}:${claim.vout} not present in parent tx`;
4118
+ }
4119
+ if (output.scriptpubkey_address !== cleanVaultAddress) {
4120
+ return `claimed output ${claim.txid}:${claim.vout} pays ${output.scriptpubkey_address ?? "<no address>"}, not the vault`;
4121
+ }
4122
+ if (output.value === void 0 || output.value !== claim.satoshis) {
4123
+ return `claimed output ${claim.txid}:${claim.vout} value ${output.value?.toString() ?? "<missing>"} != reported ${claim.satoshis.toString()}`;
4124
+ }
4125
+ }
4126
+ return null;
4127
+ }
4128
+ function stripNetworkPrefix(address) {
4129
+ const prefixPattern = /^(REGTEST_|TESTNET_|MAINNET_)/;
4130
+ return address.replace(prefixPattern, "");
4131
+ }
4132
+ var VaultBalanceConsensus = class {
4133
+ /**
4134
+ * @param minConsensusProviders Policy floor (`policy.minConsensusProviders`,
4135
+ * remediation P1/#1). Both consensus passes THROW when fewer providers are
4136
+ * configured than the floor demands — below it the cross-checks cannot run
4137
+ * and neither trusting the primary (inflation) nor excluding UTXOs
4138
+ * (deflation → wrongful liquidation) is a safe direction. A floor of 1
4139
+ * (hardhat / single-provider sepolia) preserves the trust-primary /
4140
+ * exclude-unconfirmed baseline unchanged.
4141
+ */
4142
+ constructor(primaryProvider, providerConfigs, minConsensusProviders) {
4143
+ this.primaryProvider = primaryProvider;
4144
+ this.providerConfigs = providerConfigs;
4145
+ this.minConsensusProviders = minConsensusProviders;
4146
+ this.readers = this.buildReaders();
4147
+ }
4148
+ /**
4149
+ * Fail-closed floor check shared by both consensus passes. Only floors above
4150
+ * 1 can bite: a floor of 1 with zero configs means "no esplora access
4151
+ * configured" (dev/test), where the documented pre-gate behaviour —
4152
+ * trust-primary confirmed set, exclude unconfirmed — remains correct.
4153
+ */
4154
+ assertProviderFloor(pass) {
4155
+ const configured = this.providerConfigs?.length ?? 0;
4156
+ if (this.minConsensusProviders > 1 && configured < this.minConsensusProviders) {
4157
+ throw new Error(
4158
+ `${pass}: this chain requires at least ${this.minConsensusProviders} Bitcoin providers for consensus; ${configured} configured. Refusing to proceed without cross-check.`
4159
+ );
4160
+ }
4161
+ }
4162
+ /**
4163
+ * Build one data reader per configured provider (primary first). Index 0
4164
+ * reuses the injected primary instance; the rest are constructed from their
4165
+ * configs so cross-check reads hit the right endpoint. Null when no providers
4166
+ * are configured — both passes then fall back to primary-only behaviour.
4167
+ */
4168
+ buildReaders() {
4169
+ const configs = this.providerConfigs;
4170
+ if (!configs || configs.length === 0) {
4171
+ return null;
4172
+ }
4173
+ return configs.map(
4174
+ (cfg, i) => i === 0 ? this.primaryProvider : new BitcoinDataProvider({
4175
+ providerUrl: cfg.providerUrl,
4176
+ auth: cfg.auth,
4177
+ ...cfg.timeout ? { timeout: cfg.timeout } : {}
4178
+ })
4179
+ );
4180
+ }
4181
+ /**
4182
+ * Confirmed-UTXO-set consensus across every registered provider (strict).
4183
+ *
4184
+ * The signed balance is dominated by the primary's confirmed UTXOs, but the
4185
+ * UTXO set — txid, vout, and value — is read only from the primary; the
4186
+ * `assertBitcoinBalanceConsensus` check the validators run compares
4187
+ * `chain_stats` totals, not individual outpoints. So a compromised primary
4188
+ * can add a fabricated confirmed UTXO, or inflate a real one's value, and the
4189
+ * balance consensus still passes (it serves honest `chain_stats`). This
4190
+ * closes that: each UTXO the primary reports at or above `minConfirmations`
4191
+ * must be present with the EXACT same value on every registered provider.
4192
+ *
4193
+ * Semantics:
4194
+ * - Absent on any provider, or value mismatch → THROW (fail-closed). Confirmed
4195
+ * transactions are fully propagated, so absence is unambiguous forgery, not
4196
+ * a race.
4197
+ * - Present + value-matched everywhere: the UTXO counts as confirmed only if
4198
+ * it meets `minConfirmations` on ALL providers (gated on the minimum
4199
+ * reported depth). A UTXO just past the boundary on the primary but a block
4200
+ * behind on a lagging provider is DEMOTED — excluded until it matures —
4201
+ * rather than signed over. A depth spread beyond CONFIRMATION_SKEW_TOLERANCE
4202
+ * is not benign skew but a provider lying about depth (to inflate a shallow
4203
+ * UTXO past the gate, or deflate a deep one into a wrongful liquidation) →
4204
+ * THROW.
4205
+ * - SYMMETRIC (finding #6): a confirmed outpoint any secondary reports that
4206
+ * the primary omits entirely, reports with a different value, or reports
4207
+ * at a depth more than the skew tolerance BELOW the secondary's → THROW.
4208
+ * Without this, a compromised primary deflates the balance by omission and
4209
+ * forces a wrongful liquidation.
4210
+ *
4211
+ * Below `minConsensusProviders` this THROWS (P1/#1). With a floor of 1 and
4212
+ * 0/1 providers configured, no cross-check is possible and the primary's
4213
+ * confirmed UTXOs are returned unchanged (behaviour identical to before
4214
+ * this gate).
4215
+ *
4216
+ * Cross-checking `chain_stats` totals against the confirmed-UTXO sum was
4217
+ * considered and rejected: `getBalance` reads mempool-blind `chain_stats`
4218
+ * while `getUTXOSet` removes mempool-spent outputs, so the two legitimately
4219
+ * diverge during any pending spend — precisely the self-change window — and
4220
+ * that comparison would false-fail there. Outpoint-set consensus asserts
4221
+ * agreement only on confirmed outpoints, which the mempool does not touch.
4222
+ *
4223
+ * @returns Set of "txid:vout" keys admitted as confirmed-with-consensus.
4224
+ */
4225
+ async confirmedUtxoConsensus(primaryUtxos, vaultAddress, minConfirmations) {
4226
+ const providerConfigs = this.providerConfigs;
4227
+ const readers = this.readers;
4228
+ this.assertProviderFloor("confirmedUtxoConsensus");
4229
+ const confirmedKeys = /* @__PURE__ */ new Set();
4230
+ const primaryConfirmed = primaryUtxos.filter(
4231
+ (utxo) => utxo.confirmations >= minConfirmations
4232
+ );
4233
+ for (const utxo of primaryConfirmed) {
4234
+ confirmedKeys.add(`${utxo.txid}:${utxo.vout}`);
4235
+ }
4236
+ if (!providerConfigs || providerConfigs.length <= 1 || !readers) {
4237
+ return confirmedKeys;
4238
+ }
4239
+ const providerName = (i) => providerConfigs[i].name || providerConfigs[i].providerUrl;
4240
+ const settled = await Promise.allSettled(
4241
+ readers.slice(1).map((reader) => reader.getUTXOSet(vaultAddress, minConfirmations))
4242
+ );
4243
+ const failures = settled.map((r, i) => ({ r, name: providerName(i + 1) })).filter(
4244
+ (x) => x.r.status === "rejected"
4245
+ ).map(
4246
+ ({ r, name }) => `${name} \u2192 ${r.reason?.message ?? String(r.reason)}`
4247
+ );
4248
+ if (failures.length > 0) {
4249
+ throw new Error(
4250
+ `Confirmed UTXO set read failed for ${vaultAddress} on ${failures.length}/${providerConfigs.length - 1} secondary provider(s): ${failures.join("; ")}`
4251
+ );
4252
+ }
4253
+ const secondaryMaps = settled.map((r) => {
4254
+ const map2 = /* @__PURE__ */ new Map();
4255
+ for (const utxo of r.value.utxos) {
4256
+ map2.set(`${utxo.txid}:${utxo.vout}`, {
4257
+ value: utxo.satoshis,
4258
+ confirmations: utxo.confirmations
4259
+ });
4260
+ }
4261
+ return map2;
4262
+ });
4263
+ const demoted = [];
4264
+ for (const utxo of primaryConfirmed) {
4265
+ const key = `${utxo.txid}:${utxo.vout}`;
4266
+ let minConfs = utxo.confirmations;
4267
+ let maxConfs = utxo.confirmations;
4268
+ for (let s = 0; s < secondaryMaps.length; s++) {
4269
+ const entry = secondaryMaps[s].get(key);
4270
+ const name = providerName(s + 1);
4271
+ if (!entry) {
4272
+ throw new Error(
4273
+ `Confirmed UTXO ${key} present on ${providerName(0)} but absent on ${name}; refusing to sign (possible fabricated UTXO).`
4274
+ );
4275
+ }
4276
+ if (entry.value !== utxo.satoshis) {
4277
+ throw new Error(
4278
+ `Confirmed UTXO ${key} value mismatch: ${providerName(0)}=${utxo.satoshis.toString()} vs ${name}=${entry.value.toString()}; refusing to sign.`
4279
+ );
4280
+ }
4281
+ if (entry.confirmations < minConfs)
4282
+ minConfs = entry.confirmations;
4283
+ if (entry.confirmations > maxConfs)
4284
+ maxConfs = entry.confirmations;
4285
+ }
4286
+ if (maxConfs - minConfs > CONFIRMATION_SKEW_TOLERANCE) {
4287
+ throw new Error(
4288
+ `Confirmed UTXO ${key} confirmation spread ${maxConfs - minConfs} exceeds tolerance ${CONFIRMATION_SKEW_TOLERANCE} (min ${minConfs}, max ${maxConfs}); providers disagree on depth, refusing to sign.`
4289
+ );
4290
+ }
4291
+ if (minConfs < minConfirmations) {
4292
+ confirmedKeys.delete(key);
4293
+ demoted.push(key);
4294
+ }
4295
+ }
4296
+ if (demoted.length > 0) {
4297
+ litDebugLog(
4298
+ `[VaultBalance] confirmed-consensus: ${demoted.length} UTXO(s) demoted pending provider tip skew (excluded until matured): ${demoted.join(", ")}`
4299
+ );
4300
+ }
4301
+ const primaryByKey = new Map(
4302
+ primaryUtxos.map((utxo) => [`${utxo.txid}:${utxo.vout}`, utxo])
4303
+ );
4304
+ for (let s = 0; s < secondaryMaps.length; s++) {
4305
+ const name = providerName(s + 1);
4306
+ for (const [key, entry] of secondaryMaps[s]) {
4307
+ if (entry.confirmations < minConfirmations)
4308
+ continue;
4309
+ const primaryUtxo = primaryByKey.get(key);
4310
+ if (!primaryUtxo) {
4311
+ throw new Error(
4312
+ `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).`
4313
+ );
4314
+ }
4315
+ if (primaryUtxo.satoshis !== entry.value) {
4316
+ throw new Error(
4317
+ `Confirmed UTXO ${key} value mismatch: ${providerName(0)}=${primaryUtxo.satoshis.toString()} vs ${name}=${entry.value.toString()}; refusing to sign.`
4318
+ );
4319
+ }
4320
+ if (entry.confirmations - primaryUtxo.confirmations > CONFIRMATION_SKEW_TOLERANCE) {
4321
+ throw new Error(
4322
+ `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.`
4323
+ );
4324
+ }
4325
+ }
4326
+ }
4327
+ return confirmedKeys;
4328
+ }
4329
+ /**
4330
+ * Classify unconfirmed UTXOs as protocol self-change.
4331
+ *
4332
+ * A UTXO below minConfirmations counts as confirmed-equivalent ONLY when the
4333
+ * transaction that created it spends exclusively vault-owned, confirmed
4334
+ * outputs. Such a transaction can only have been signed by the vault's PKP,
4335
+ * so no third party can RBF-replace or double-spend it — the reversal risk
4336
+ * the confirmation gate exists to close does not apply. Any non-vault input,
4337
+ * unconfirmed funding tx, or coinbase input disqualifies the UTXO
4338
+ * (fail-safe: exclusion, never inclusion).
4339
+ *
4340
+ * Multi-provider consensus (audit C-1/C-2 parity): the parent-tx proof is
4341
+ * verified independently against EVERY configured provider and admitted
4342
+ * only on unanimity. The confirmed-balance consensus the validators run
4343
+ * covers `chain_stats` only, so without this the primary provider alone
4344
+ * could fabricate an unconfirmed UTXO plus a self-consistent parent proof
4345
+ * and inflate the signed balance. Any provider failure or verdict
4346
+ * disagreement THROWS (fail-closed) — exclusion on a suppressed proof
4347
+ * would deflate the balance and open a wrongful-liquidation window, so
4348
+ * neither direction may proceed without agreement.
4349
+ *
4350
+ * The same per-provider `/tx` fetch also returns the parent's outputs, and
4351
+ * every provider must corroborate each claimed UTXO (index, vault address,
4352
+ * exact value) before admission — the UTXO list itself comes only from the
4353
+ * primary, so an unchecked claim would let it attach a fabricated or
4354
+ * inflated output to a genuine self-spend.
4355
+ *
4356
+ * Fetch cost: zero when every UTXO is confirmed (the common case); otherwise
4357
+ * per provider: one tx lookup per classified parent (vins + vouts in one
4358
+ * response), plus per distinct funding tx a confirmation check and — only
4359
+ * when the provider omits vin prevout data — an output lookup. Classified
4360
+ * parents are capped at MAX_CLASSIFIED_PARENTS (largest claimed value
4361
+ * first, deterministic) and evaluated CLASSIFICATION_CONCURRENCY at a time,
4362
+ * so third-party dust spam cannot amplify into provider rate-limiting and
4363
+ * an action-wide outage.
4364
+ *
4365
+ * See docs/vault-balance-confirmed-gate-plan-2026-07-14.md §3.1.
4366
+ *
4367
+ * @param utxos - Full UTXO set for the vault address
4368
+ * @param vaultAddress - Bitcoin address of the vault (may carry network prefix)
4369
+ * @param minConfirmations - Confirmation threshold for the chain
4370
+ * @returns Set of "txid:vout" keys for UTXOs verified as self-change
4371
+ */
4372
+ async classifySelfChange(utxos, vaultAddress, minConfirmations) {
4373
+ const providerConfigs = this.providerConfigs;
4374
+ const readers = this.readers;
4375
+ const selfChange = /* @__PURE__ */ new Set();
4376
+ const unconfirmed = utxos.filter(
4377
+ (utxo) => utxo.confirmations < minConfirmations
4378
+ );
4379
+ if (unconfirmed.length === 0) {
4380
+ return selfChange;
4381
+ }
4382
+ this.assertProviderFloor("classifySelfChange");
4383
+ if (!providerConfigs || providerConfigs.length === 0 || !readers) {
4384
+ litDebugLog(
4385
+ `[VaultBalance] ${unconfirmed.length} unconfirmed UTXO(s) excluded: no bitcoinProviderConfigs for self-change classification`
4386
+ );
4387
+ return selfChange;
4388
+ }
4389
+ const cleanVaultAddress = stripNetworkPrefix(vaultAddress);
4390
+ const claimsByParent = /* @__PURE__ */ new Map();
4391
+ for (const utxo of unconfirmed) {
4392
+ const claims = claimsByParent.get(utxo.txid) ?? [];
4393
+ claims.push(utxo);
4394
+ claimsByParent.set(utxo.txid, claims);
4395
+ }
4396
+ const claimedSum = (txid) => claimsByParent.get(txid).reduce((sum, utxo) => sum + utxo.satoshis, 0n);
4397
+ const rankedParents = [...claimsByParent.keys()].sort((a, b) => {
4398
+ const diff = claimedSum(b) - claimedSum(a);
4399
+ if (diff !== 0n) {
4400
+ return diff > 0n ? 1 : -1;
4401
+ }
4402
+ return a < b ? -1 : a > b ? 1 : 0;
4403
+ });
4404
+ const parentTxids = rankedParents.slice(0, MAX_CLASSIFIED_PARENTS);
4405
+ const skippedParents = rankedParents.length - parentTxids.length;
4406
+ if (skippedParents > 0) {
4407
+ litDebugLog(
4408
+ `[VaultBalance] self-change: ${skippedParents} parent tx(s) beyond the ${MAX_CLASSIFIED_PARENTS}-parent classification budget excluded (mempool-spam bound; largest claimed value classified first)`
4409
+ );
4410
+ }
4411
+ const verdicts = await mapWithConcurrency(
4412
+ parentTxids,
4413
+ CLASSIFICATION_CONCURRENCY,
4414
+ async (parentTxid) => ({
4415
+ parentTxid,
4416
+ verified: await this.verifyParentIsSelfSpendConsensus(
4417
+ parentTxid,
4418
+ claimsByParent.get(parentTxid),
4419
+ cleanVaultAddress,
4420
+ minConfirmations,
4421
+ providerConfigs,
4422
+ readers
4423
+ )
4424
+ })
4425
+ );
4426
+ const verifiedParents = new Set(
4427
+ verdicts.filter((v) => v.verified).map((v) => v.parentTxid)
4428
+ );
4429
+ for (const utxo of unconfirmed) {
4430
+ if (verifiedParents.has(utxo.txid)) {
4431
+ selfChange.add(`${utxo.txid}:${utxo.vout}`);
4432
+ }
4433
+ }
4434
+ if (verifiedParents.size > 0) {
4435
+ litDebugLog(
4436
+ `[VaultBalance] self-change: ${selfChange.size}/${unconfirmed.length} unconfirmed UTXO(s) verified as protocol change by ${providerConfigs.length} provider(s)`
4437
+ );
4438
+ }
4439
+ return selfChange;
4440
+ }
4441
+ /**
4442
+ * Run the self-spend proof against every configured provider and require
4443
+ * unanimity. Fail-closed: a provider request failure or a verdict
4444
+ * disagreement throws and aborts the balance calculation — see
4445
+ * classifySelfChange for why exclusion is not a safe fallback here.
4446
+ *
4447
+ * When the parent IS a unanimous self-spend, every provider's view of the
4448
+ * parent's OUTPUTS must additionally corroborate the primary's claimed
4449
+ * UTXOs (index exists, pays the vault, exact value). Without this, a
4450
+ * compromised primary could attach a fabricated or value-inflated output
4451
+ * to a genuine self-spend the honest providers happily verify.
4452
+ *
4453
+ * @returns True when every provider verified the parent and corroborated
4454
+ * the claimed outputs, false when every provider rejected the
4455
+ * parent (genuine third-party deposit)
4456
+ */
4457
+ async verifyParentIsSelfSpendConsensus(parentTxid, claimedUtxos, cleanVaultAddress, minConfirmations, providerConfigs, readers) {
4458
+ const providerName = (i) => providerConfigs[i].name || providerConfigs[i].providerUrl;
4459
+ const settled = await Promise.allSettled(
4460
+ providerConfigs.map(
4461
+ (cfg, i) => this.evaluateParentForProvider(
4462
+ parentTxid,
4463
+ claimedUtxos,
4464
+ cleanVaultAddress,
4465
+ minConfirmations,
4466
+ readers[i],
4467
+ cfg
4468
+ )
4469
+ )
4470
+ );
4471
+ const failures = settled.map((r, i) => ({ r, name: providerName(i) })).filter(
4472
+ (x) => x.r.status === "rejected"
4473
+ ).map(
4474
+ ({ r, name }) => `${name} \u2192 ${r.reason?.message ?? String(r.reason)}`
4475
+ );
4476
+ if (failures.length > 0) {
4477
+ throw new Error(
4478
+ `Self-change classification failed for tx ${parentTxid} on ${failures.length}/${providerConfigs.length} provider(s): ${failures.join("; ")}`
4479
+ );
4480
+ }
4481
+ const evaluations = settled.map(
4482
+ (r) => r.value
4483
+ );
4484
+ const votes = evaluations.map((e) => e.selfSpend);
4485
+ const disagreements = votes.map((vote, i) => ({ vote, name: providerName(i) })).filter((x) => x.vote !== votes[0]).map(
4486
+ (x) => `${providerName(0)}=${votes[0]} vs ${x.name}=${x.vote}`
4487
+ );
4488
+ if (disagreements.length > 0) {
4489
+ throw new Error(
4490
+ `Self-change consensus mismatch for tx ${parentTxid}: ${disagreements.join("; ")}. A provider may be compromised or out of sync; refusing to sign.`
4491
+ );
4492
+ }
4493
+ if (!votes[0]) {
4494
+ return false;
4495
+ }
4496
+ const outputMismatches = evaluations.map((e, i) => e.outputMismatch ? `${providerName(i)} \u2192 ${e.outputMismatch}` : null).filter((m) => m !== null);
4497
+ if (outputMismatches.length > 0) {
4498
+ throw new Error(
4499
+ `Self-change output claim mismatch for tx ${parentTxid}: ${outputMismatches.join("; ")}. The reported UTXO set contradicts the parent transaction; refusing to sign.`
4500
+ );
4501
+ }
4502
+ return true;
4503
+ }
4504
+ /**
4505
+ * Single provider's evaluation of a parent tx: the self-spend proof plus —
4506
+ * when the proof holds — corroboration of the claimed outputs against this
4507
+ * provider's view of the parent's vout set. The output check is skipped for
4508
+ * non-self-spend parents (their UTXOs are excluded regardless, and a
4509
+ * third-party deposit's values are not ours to police).
4510
+ */
4511
+ async evaluateParentForProvider(parentTxid, claimedUtxos, cleanVaultAddress, minConfirmations, reader, providerConfig) {
4512
+ const parentTx = await this.fetchParentTransaction(
4513
+ parentTxid,
4514
+ providerConfig
4515
+ );
4516
+ if (!parentTx || parentTx.inputs.length === 0) {
4517
+ return { selfSpend: false, outputMismatch: null };
4518
+ }
4519
+ const selfSpend = await this.verifyInputsAreSelfSpend(
4520
+ parentTx.inputs,
4521
+ cleanVaultAddress,
4522
+ minConfirmations,
4523
+ reader
4524
+ );
4525
+ const outputMismatch = selfSpend ? findOutputClaimMismatch(claimedUtxos, parentTx.outputs, cleanVaultAddress) : null;
4526
+ return { selfSpend, outputMismatch };
4527
+ }
4528
+ /**
4529
+ * Verify that a transaction's inputs spend exclusively vault-owned,
4530
+ * confirmed outputs, using a single provider's view of the chain.
4531
+ *
4532
+ * @param inputs - Parsed inputs of the parent transaction
4533
+ * @param cleanVaultAddress - Vault address with network prefix stripped
4534
+ * @param minConfirmations - Required depth for every funding transaction
4535
+ * @param reader - Data provider for funding-tx lookups (same provider the inputs came from)
4536
+ * @returns True only when every input is a confirmed, vault-owned outpoint
4537
+ */
4538
+ async verifyInputsAreSelfSpend(inputs, cleanVaultAddress, minConfirmations, reader) {
4539
+ const fundingTxids = [...new Set(inputs.map((input) => input.txid))];
4540
+ const outputsByFundingTx = /* @__PURE__ */ new Map();
4541
+ const needsOutputLookup = fundingTxids.filter(
4542
+ (fundingTxid) => inputs.some(
4543
+ (input) => input.txid === fundingTxid && input.prevoutAddress === void 0
4544
+ )
4545
+ );
4546
+ await Promise.all(
4547
+ needsOutputLookup.map(async (fundingTxid) => {
4548
+ outputsByFundingTx.set(
4549
+ fundingTxid,
4550
+ await reader.getTransactionOutputs(fundingTxid)
4551
+ );
4552
+ })
4553
+ );
4554
+ for (const input of inputs) {
4555
+ let ownerAddress = input.prevoutAddress;
4556
+ if (ownerAddress === void 0) {
4557
+ const outputs = outputsByFundingTx.get(input.txid);
4558
+ ownerAddress = outputs?.find(
4559
+ (output) => output.index === input.vout
4560
+ )?.scriptpubkey_address;
4561
+ }
4562
+ if (ownerAddress !== cleanVaultAddress) {
4563
+ return false;
4564
+ }
4565
+ }
4566
+ const fundingStatuses = await Promise.all(
4567
+ fundingTxids.map((fundingTxid) => reader.getTransaction(fundingTxid))
4568
+ );
4569
+ for (const status of fundingStatuses) {
4570
+ if (!status || status.confirmations < minConfirmations) {
4571
+ return false;
4572
+ }
4573
+ }
4574
+ return true;
4575
+ }
4576
+ /**
4577
+ * Fetch a parent transaction's inputs (vins) and outputs (vouts) via Esplora.
4578
+ *
4579
+ * A module-private fetch (not a `BitcoinDataProvider` method) so editing this
4580
+ * consensus logic does not rebundle the shared provider module's other
4581
+ * importers — see this file's header for the ripple-set constraint.
4582
+ *
4583
+ * @param txid - Transaction ID
4584
+ * @param providerConfig - Esplora endpoint to query (one of the configured
4585
+ * consensus providers)
4586
+ * @returns Parsed inputs + outputs, or null ONLY when the tx is provably not
4587
+ * a self-spend candidate (coinbase-funded). Provider inability —
4588
+ * 404, malformed vin rows, missing vin array — THROWS (review
4589
+ * 2026-07-15 P1): the parent of a UTXO the primary lists must
4590
+ * exist, so "can't fetch it" is a provider failure, and treating it
4591
+ * as a false self-change vote would silently deflate collateral
4592
+ * (wrongful-liquidation direction) if every provider failed the
4593
+ * same way.
4594
+ */
4595
+ async fetchParentTransaction(txid, providerConfig) {
4596
+ if (!providerConfig.providerUrl) {
4597
+ return null;
4598
+ }
4599
+ const timeout = providerConfig.timeout || 15e3;
4600
+ const controller = new AbortController();
4601
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
4602
+ try {
4603
+ const response = await fetch(`${providerConfig.providerUrl}/tx/${txid}`, {
4604
+ signal: controller.signal,
4605
+ headers: await authHeaders(providerConfig.auth, timeout)
4606
+ });
4607
+ if (!response.ok) {
4608
+ if (response.status === 404) {
4609
+ throw new Error(
4610
+ `parent tx ${txid} not found on this provider \u2014 cannot verify the self-change proof`
4611
+ );
4612
+ }
4613
+ throw new Error(
4614
+ `Bitcoin provider error: ${response.status} ${response.statusText}`
4615
+ );
4616
+ }
4617
+ const data = await response.json();
4618
+ if (!data.vin || !Array.isArray(data.vin) || data.vin.length === 0) {
4619
+ throw new Error(
4620
+ `provider returned tx ${txid} without a vin array \u2014 malformed response`
4621
+ );
4622
+ }
4623
+ const inputs = [];
4624
+ for (const vin of data.vin) {
4625
+ if (vin.is_coinbase) {
4626
+ return null;
4627
+ }
4628
+ if (vin.txid === void 0 || vin.vout === void 0) {
4629
+ throw new Error(
4630
+ `provider returned tx ${txid} with a malformed vin row (missing txid/vout)`
4631
+ );
4632
+ }
4633
+ inputs.push({
4634
+ txid: vin.txid,
4635
+ vout: vin.vout,
4636
+ prevoutAddress: vin.prevout?.scriptpubkey_address
4637
+ });
4638
+ }
4639
+ const outputs = Array.isArray(data.vout) ? data.vout.map((vout, index) => ({
4640
+ index,
4641
+ scriptpubkey_address: vout?.scriptpubkey_address,
4642
+ value: typeof vout?.value === "number" ? BigInt(vout.value) : void 0
4643
+ })) : [];
4644
+ return { inputs, outputs };
4645
+ } catch (error) {
4646
+ if (isAbortError(error)) {
4647
+ throw new Error(`Request timeout after ${timeout}ms`);
4648
+ }
4649
+ throw error;
4650
+ } finally {
4651
+ clearTimeout(timeoutId);
4652
+ }
4653
+ }
4654
+ };
4073
4655
  var VaultBalanceModule = class {
4074
4656
  constructor(config) {
4657
+ if (!Number.isInteger(config.minConfirmations) || config.minConfirmations < 1) {
4658
+ throw new Error(
4659
+ `VaultBalanceModule: minConfirmations must be a positive integer (got ${config.minConfirmations}). Pass the policy-derived balance-confirmation threshold \u2014 there is no default.`
4660
+ );
4661
+ }
4075
4662
  this.config = config;
4076
4663
  this.bitcoinProvider = config.bitcoinProvider;
4077
4664
  }
4078
4665
  /**
4079
- * Calculate balance (fast path - no UTXO validation)
4666
+ * Calculate balance (arithmetic only - no UTXO validation)
4080
4667
  *
4081
- * Fast balance-only calculation that skips UTXO validation.
4082
- * Uses simple arithmetic: Total Balance - Authorized Balance = Available Balance
4668
+ * Balance-only calculation: Total Balance - Authorized Balance = Available.
4083
4669
  *
4084
- * Use this when you only need the balance amount and don't need:
4085
- * - Individual UTXO details
4086
- * - Validation that authorized UTXOs were actually spent
4670
+ * ⚠️ SECURITY (remediation P1/#5): this result skips UTXO validation, the
4671
+ * confirmed-consensus gate, and self-change proofing. It must NEVER feed a
4672
+ * signed BalanceConfirmation or any authorization decision. No production
4673
+ * action calls it (the `skipUTXOValidation` fast path that used to route
4674
+ * here was deleted); it exists for read-only diagnostics and tests. Adding
4675
+ * a production caller requires Master sign-off.
4087
4676
  *
4088
4677
  * @param positionId - The position ID to check
4089
4678
  * @param vaultAddress - Bitcoin address of the vault
@@ -4130,17 +4719,18 @@ ${auth.clientId}`;
4130
4719
  * 1. Authorized transaction not found on network (not broadcasted)
4131
4720
  * 2. Authorized transaction has insufficient confirmations (network updating)
4132
4721
  *
4722
+ * NOTE: the `skipUTXOValidation` fast path was removed (remediation P1/#5).
4723
+ * Every trusted-balance read runs full UTXO validation + consensus; a
4724
+ * balance that skipped validation must never feed a signed
4725
+ * BalanceConfirmation.
4726
+ *
4133
4727
  * @param positionId - The position ID to check
4134
4728
  * @param vaultAddress - Bitcoin address of the vault
4135
- * @param skipUTXOValidation - If true, use fast balance-only path (skips UTXO validation)
4136
4729
  * @returns Complete breakdown of vault balance
4137
4730
  * @throws Error if authorized spends are in invalid state
4138
4731
  */
4139
- async calculateTrustedBalance(positionId, vaultAddress, skipUTXOValidation) {
4140
- if (skipUTXOValidation) {
4141
- return await this.calculateBalance(positionId, vaultAddress);
4142
- }
4143
- const minConfirmations = this.config.minConfirmations || 6;
4732
+ async calculateTrustedBalance(positionId, vaultAddress) {
4733
+ const minConfirmations = this.config.minConfirmations;
4144
4734
  const utxoSet = await this.bitcoinProvider.getUTXOSet(
4145
4735
  vaultAddress,
4146
4736
  minConfirmations
@@ -4160,8 +4750,25 @@ ${auth.clientId}`;
4160
4750
  (sum, spend) => sum + spend.satoshis,
4161
4751
  0n
4162
4752
  );
4753
+ const consensus = new VaultBalanceConsensus(
4754
+ this.bitcoinProvider,
4755
+ this.config.bitcoinProviderConfigs,
4756
+ // Absent ⇒ 1: dev/test constructions without a policy keep the
4757
+ // single-provider baseline; validators pass policy.minConsensusProviders.
4758
+ this.config.minConsensusProviders ?? 1
4759
+ );
4760
+ const confirmedConsensusKeys = await consensus.confirmedUtxoConsensus(
4761
+ utxoSet.utxos,
4762
+ vaultAddress,
4763
+ minConfirmations
4764
+ );
4765
+ const selfChangeKeys = await consensus.classifySelfChange(
4766
+ utxoSet.utxos,
4767
+ vaultAddress,
4768
+ minConfirmations
4769
+ );
4163
4770
  const availableUTXOs = utxoSet.utxos.filter(
4164
- (utxo) => !this.isUTXOAuthorized(utxo, authorizedSpends)
4771
+ (utxo) => (confirmedConsensusKeys.has(`${utxo.txid}:${utxo.vout}`) || selfChangeKeys.has(`${utxo.txid}:${utxo.vout}`)) && !this.isUTXOAuthorized(utxo, authorizedSpends)
4165
4772
  );
4166
4773
  const availableBalance = availableUTXOs.reduce(
4167
4774
  (sum, utxo) => sum + utxo.satoshis,
@@ -4393,6 +5000,76 @@ ${auth.clientId}`;
4393
5000
  function createVaultBalanceModule(config) {
4394
5001
  return new VaultBalanceModule(config);
4395
5002
  }
5003
+ var POLICY_BY_CHAIN = {
5004
+ [
5005
+ "hardhat"
5006
+ /* HARDHAT */
5007
+ ]: {
5008
+ chain: "hardhat",
5009
+ chainId: CHAIN_TO_CHAIN_ID[
5010
+ "hardhat"
5011
+ /* HARDHAT */
5012
+ ],
5013
+ bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
5014
+ "hardhat"
5015
+ /* HARDHAT */
5016
+ ],
5017
+ allowArbitraryBitcoinProvider: true,
5018
+ allowArbitraryEvmRpc: true,
5019
+ allowDevSinglePriceProvider: true,
5020
+ minBitcoinBalanceConfirmations: 1,
5021
+ minBitcoinSpendConfirmations: 1,
5022
+ minConsensusProviders: 1
5023
+ },
5024
+ [
5025
+ "sepolia"
5026
+ /* SEPOLIA */
5027
+ ]: {
5028
+ chain: "sepolia",
5029
+ chainId: CHAIN_TO_CHAIN_ID[
5030
+ "sepolia"
5031
+ /* SEPOLIA */
5032
+ ],
5033
+ bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
5034
+ "sepolia"
5035
+ /* SEPOLIA */
5036
+ ],
5037
+ allowArbitraryBitcoinProvider: false,
5038
+ allowArbitraryEvmRpc: false,
5039
+ allowDevSinglePriceProvider: false,
5040
+ minBitcoinBalanceConfirmations: 1,
5041
+ minBitcoinSpendConfirmations: 1,
5042
+ minConsensusProviders: 1
5043
+ },
5044
+ [
5045
+ "ethereum"
5046
+ /* ETHEREUM */
5047
+ ]: {
5048
+ chain: "ethereum",
5049
+ chainId: CHAIN_TO_CHAIN_ID[
5050
+ "ethereum"
5051
+ /* ETHEREUM */
5052
+ ],
5053
+ bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
5054
+ "ethereum"
5055
+ /* ETHEREUM */
5056
+ ],
5057
+ allowArbitraryBitcoinProvider: false,
5058
+ allowArbitraryEvmRpc: false,
5059
+ allowDevSinglePriceProvider: false,
5060
+ minBitcoinBalanceConfirmations: 6,
5061
+ minBitcoinSpendConfirmations: 6,
5062
+ minConsensusProviders: 2
5063
+ }
5064
+ };
5065
+ function getChainPolicy(chain) {
5066
+ const validated = validateChain(chain);
5067
+ const policy = POLICY_BY_CHAIN[validated];
5068
+ if (!policy) {
5069
+ throw new Error(`No chain policy registered for "${chain}"`);
5070
+ }
5071
+ return policy;
5072
+ }
4396
5073
  async function executeVaultSnapshot(params) {
4397
5074
  global.ethers = {
4398
5075
  ...import_ethers16.ethers,
@@ -4437,7 +5114,7 @@ ${auth.clientId}`;
4437
5114
  // Default to mainnet if not provided
4438
5115
  rpcUrl: params.rpcUrl,
4439
5116
  bitcoinProvider,
4440
- minConfirmations: params.minConfirmations || 6
5117
+ minConfirmations: params.minConfirmations ?? getChainPolicy(params.chain).minBitcoinBalanceConfirmations
4441
5118
  });
4442
5119
  const vaultSnapshot = createVaultSnapshotModule({
4443
5120
  contractAddress: params.contractAddress,
@@ -4671,7 +5348,8 @@ function getSepoliaConfig() {
4671
5348
  contractVersionRegistry: SEPOLIA_CONTRACTS.ContractVersionRegistry || "",
4672
5349
  feeRecipientRegistry: SEPOLIA_CONTRACTS.FeeRecipientRegistry || "",
4673
5350
  positionDelegateRegistry: SEPOLIA_CONTRACTS.PositionDelegateRegistry || "",
4674
- bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
5351
+ bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
5352
+ carryAgentRegistry: SEPOLIA_CONTRACTS.CarryAgentRegistry || ""
4675
5353
  },
4676
5354
  subgraphs: {
4677
5355
  // Lazy getter for the same module-init snapshot reason as serviceEndpoint.
@@ -4711,7 +5389,8 @@ function getMainnetConfig() {
4711
5389
  contractVersionRegistry: MAINNET_CONTRACTS.ContractVersionRegistry || "",
4712
5390
  feeRecipientRegistry: MAINNET_CONTRACTS.FeeRecipientRegistry || "",
4713
5391
  positionDelegateRegistry: MAINNET_CONTRACTS.PositionDelegateRegistry || "",
4714
- bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
5392
+ bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
5393
+ carryAgentRegistry: MAINNET_CONTRACTS.CarryAgentRegistry || ""
4715
5394
  },
4716
5395
  subgraphs: {
4717
5396
  diamondHandsUrl: "https://gateway-arbitrum.network.thegraph.com/api/subgraphs/id/CCTPsdYqco2jChDLLBQTbdJWwoukVoMt1cXeR9ti6r9A"
@@ -4851,7 +5530,7 @@ var init_network_configs = __esm({
4851
5530
  });
4852
5531
 
4853
5532
  // src/modules/diamond-hands-sdk.ts
4854
- import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2565, solidityPackedKeccak256 as solidityPackedKeccak2564, toUtf8Bytes as toUtf8Bytes5, zeroPadValue as zeroPadValue4, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3, MaxUint256 } from "ethers";
5533
+ import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2565, solidityPackedKeccak256 as solidityPackedKeccak2564, toUtf8Bytes as toUtf8Bytes5, getBytes as getBytes4, zeroPadValue as zeroPadValue4, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3, MaxUint256 } from "ethers";
4855
5534
 
4856
5535
  // src/types/result.ts
4857
5536
  function success(value) {
@@ -5748,26 +6427,144 @@ async function buildSignedLoginPayload(signer, chainId, audience) {
5748
6427
  return { chainId, message, signature };
5749
6428
  }
5750
6429
 
6430
+ // src/utils/server-session-store.ts
6431
+ var REAUTH_WINDOW_SECONDS = 24 * 60 * 60;
6432
+ var SAFETY_MARGIN_SECONDS = 5 * 60;
6433
+ var STORE_PREFIX = "dh-server-session:";
6434
+ function sessionStoreKey(address, chainId, endpoint) {
6435
+ return STORE_PREFIX + `${address.trim().toLowerCase()}:${chainId}:${endpoint.replace(/\/+$/, "")}`;
6436
+ }
6437
+ function isPayloadShape(value) {
6438
+ 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";
6439
+ }
6440
+ function isPersistedShape(value) {
6441
+ 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");
6442
+ }
6443
+ var LocalStorageSessionStore = class {
6444
+ load(key) {
6445
+ let raw;
6446
+ try {
6447
+ raw = window.localStorage.getItem(key);
6448
+ } catch {
6449
+ return null;
6450
+ }
6451
+ if (!raw)
6452
+ return null;
6453
+ let parsed;
6454
+ try {
6455
+ parsed = JSON.parse(raw);
6456
+ } catch {
6457
+ this.clear(key);
6458
+ return null;
6459
+ }
6460
+ if (!isPersistedShape(parsed)) {
6461
+ this.clear(key);
6462
+ return null;
6463
+ }
6464
+ return parsed;
6465
+ }
6466
+ save(key, session) {
6467
+ try {
6468
+ window.localStorage.setItem(key, JSON.stringify(session));
6469
+ } catch {
6470
+ }
6471
+ }
6472
+ clear(key) {
6473
+ try {
6474
+ window.localStorage.removeItem(key);
6475
+ } catch {
6476
+ }
6477
+ }
6478
+ };
6479
+ var MemorySessionStore = class {
6480
+ entries = /* @__PURE__ */ new Map();
6481
+ load(key) {
6482
+ return this.entries.get(key) ?? null;
6483
+ }
6484
+ save(key, session) {
6485
+ this.entries.set(key, session);
6486
+ }
6487
+ clear(key) {
6488
+ this.entries.delete(key);
6489
+ }
6490
+ };
6491
+ function localStorageUsable() {
6492
+ try {
6493
+ if (typeof window === "undefined" || !window.localStorage)
6494
+ return false;
6495
+ const probe = "__dh_server_session_probe__";
6496
+ window.localStorage.setItem(probe, "1");
6497
+ window.localStorage.removeItem(probe);
6498
+ return true;
6499
+ } catch {
6500
+ return false;
6501
+ }
6502
+ }
6503
+ function createDefaultSessionStore() {
6504
+ return localStorageUsable() ? new LocalStorageSessionStore() : new MemorySessionStore();
6505
+ }
6506
+
5751
6507
  // src/utils/server-session.ts
5752
6508
  var REFRESH_LEEWAY_SECONDS = 30;
6509
+ var ServerLoginError = class extends Error {
6510
+ constructor(message, status) {
6511
+ super(message);
6512
+ this.status = status;
6513
+ this.name = "ServerLoginError";
6514
+ }
6515
+ };
6516
+ function isLoginRejection(error) {
6517
+ return error instanceof ServerLoginError && error.status >= 400 && error.status < 500 && error.status !== 429;
6518
+ }
5753
6519
  var ServerSession = class {
5754
6520
  signer;
5755
6521
  serviceEndpoint;
5756
6522
  chainId;
6523
+ store;
6524
+ onSignaturePrompt;
5757
6525
  now;
5758
6526
  fetchImpl;
5759
6527
  cached = null;
5760
6528
  inFlight = null;
6529
+ /**
6530
+ * Bumped by `clearPersisted()` (logout / disconnect). An in-flight
6531
+ * `getOrRefresh()` captures this at start and refuses to write its freshly
6532
+ * minted token back to `this.cached`/the store if the value changed
6533
+ * meanwhile — so a refresh racing a sign-out can't silently "un-clear" the
6534
+ * credential it was told to drop.
6535
+ */
6536
+ generation = 0;
6537
+ /**
6538
+ * Store key for the current signer, memoized on first use so the sync
6539
+ * `clear()` can drop the persisted token without an async address lookup.
6540
+ */
6541
+ lastStoreKey = null;
5761
6542
  constructor(opts) {
5762
6543
  assertSafeServiceEndpoint(opts.serviceEndpoint);
5763
6544
  this.signer = opts.signer;
5764
6545
  this.serviceEndpoint = opts.serviceEndpoint.replace(/\/+$/, "");
5765
6546
  this.chainId = opts.chainId;
6547
+ this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6548
+ this.onSignaturePrompt = opts.onSignaturePrompt;
5766
6549
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
5767
6550
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
5768
6551
  }
5769
- /** Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds. */
5770
- async getValidToken() {
6552
+ /**
6553
+ * Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds.
6554
+ * Pass `{ silent: true }` to forbid a fresh wallet signature: it resolves ONLY
6555
+ * from a live cached token or the persisted re-mint envelope, never prompting
6556
+ * and never joining a prompt-capable in-flight refresh (used by `logout()`,
6557
+ * which must never pop the wallet during sign-out). Rejects when only a fresh
6558
+ * signature could produce a token.
6559
+ */
6560
+ async getValidToken(opts) {
6561
+ if (opts?.silent) {
6562
+ const session2 = await this.tryResolveSilently(this.generation);
6563
+ if (!session2) {
6564
+ throw new Error("ServerSession: no existing session to refresh");
6565
+ }
6566
+ return session2.token;
6567
+ }
5771
6568
  const session = await this.getOrRefresh();
5772
6569
  return session.token;
5773
6570
  }
@@ -5775,19 +6572,61 @@ var ServerSession = class {
5775
6572
  async getAuthHeader() {
5776
6573
  return { Authorization: `Bearer ${await this.getValidToken()}` };
5777
6574
  }
5778
- /** Drop the cached token (e.g. on 401 from server). Next call re-logs in. */
6575
+ /**
6576
+ * Drop the cached token (e.g. on 401 from server). The persisted envelope is
6577
+ * kept — the next call re-mints silently — but its stored token copy is
6578
+ * dropped too so a dead JWT can't be re-adopted from the store.
6579
+ */
5779
6580
  clear() {
5780
6581
  this.cached = null;
5781
6582
  this.inFlight = null;
6583
+ if (!this.store || !this.lastStoreKey)
6584
+ return;
6585
+ const persisted = this.store.load(this.lastStoreKey);
6586
+ if (persisted?.token) {
6587
+ this.store.save(this.lastStoreKey, {
6588
+ ...persisted,
6589
+ token: void 0,
6590
+ tokenExpiresAt: void 0
6591
+ });
6592
+ }
6593
+ }
6594
+ /**
6595
+ * Drop the session AND the persisted re-mint envelope (wallet disconnect /
6596
+ * explicit sign-out paths — anything that must remove the 24h credential
6597
+ * from storage).
6598
+ */
6599
+ async clearPersisted() {
6600
+ this.generation += 1;
6601
+ this.cached = null;
6602
+ this.inFlight = null;
6603
+ if (!this.store)
6604
+ return;
6605
+ try {
6606
+ if (this.lastStoreKey) {
6607
+ this.store.clear(this.lastStoreKey);
6608
+ } else {
6609
+ const address = (await this.signer.getAddress()).toLowerCase();
6610
+ this.store.clear(
6611
+ sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6612
+ );
6613
+ }
6614
+ } catch {
6615
+ } finally {
6616
+ this.generation += 1;
6617
+ this.cached = null;
6618
+ }
5782
6619
  }
5783
6620
  /**
5784
- * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`).
5785
- * Clears the local cache regardless of server response so the client stops
5786
- * presenting the token.
6621
+ * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`)
6622
+ * which also tombstones the envelope's re-mint window server-side and
6623
+ * remove the persisted credential locally. Clears the local cache regardless
6624
+ * of server response so the client stops presenting the token.
5787
6625
  */
5788
6626
  async logout() {
5789
- const token = this.cached?.token ?? await this.getValidToken().catch(() => null);
5790
- this.clear();
6627
+ const cached = this.cached;
6628
+ const token = cached && cached.expiresAt > this.now() ? cached.token : await this.getValidToken({ silent: true }).catch(() => null);
6629
+ await this.clearPersisted();
5791
6630
  if (!token)
5792
6631
  return;
5793
6632
  const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/logout`, {
@@ -5801,6 +6640,50 @@ var ServerSession = class {
5801
6640
  );
5802
6641
  }
5803
6642
  }
6643
+ /**
6644
+ * Resolve a session WITHOUT ever prompting for a wallet signature: a live
6645
+ * cached token, else the persisted JWT (adopted if still fresh, otherwise
6646
+ * silently re-minted from the envelope). Returns `null` only when a fresh
6647
+ * wallet signature would be required (dead/absent envelope). Never touches
6648
+ * `this.inFlight`, so a silent caller (logout) can resolve independently
6649
+ * instead of being dragged into a prompt-capable refresh already in flight.
6650
+ *
6651
+ * `gen` is the caller's clear-generation snapshot: writes to `this.cached`
6652
+ * and the store are skipped if a `clearPersisted()` landed meanwhile.
6653
+ */
6654
+ async tryResolveSilently(gen) {
6655
+ const nowSec = this.now();
6656
+ if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
6657
+ return this.cached;
6658
+ }
6659
+ const persisted = await this.loadPersisted();
6660
+ if (!persisted)
6661
+ return null;
6662
+ if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6663
+ const adopted = {
6664
+ token: persisted.token,
6665
+ expiresAt: persisted.tokenExpiresAt
6666
+ };
6667
+ if (gen === this.generation)
6668
+ this.cached = adopted;
6669
+ return adopted;
6670
+ }
6671
+ try {
6672
+ const session = await this.login(persisted.payload);
6673
+ if (gen === this.generation) {
6674
+ this.persist(persisted.payload, session);
6675
+ this.cached = session;
6676
+ }
6677
+ return session;
6678
+ } catch (error) {
6679
+ if (!isLoginRejection(error))
6680
+ throw error;
6681
+ if (gen === this.generation && this.store && this.lastStoreKey) {
6682
+ this.store.clear(this.lastStoreKey);
6683
+ }
6684
+ return null;
6685
+ }
6686
+ }
5804
6687
  async getOrRefresh() {
5805
6688
  const nowSec = this.now();
5806
6689
  if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
@@ -5808,39 +6691,97 @@ var ServerSession = class {
5808
6691
  }
5809
6692
  if (this.inFlight)
5810
6693
  return this.inFlight;
6694
+ const gen = this.generation;
6695
+ const notCleared = () => gen === this.generation;
5811
6696
  this.inFlight = (async () => {
5812
6697
  try {
6698
+ const existing = await this.tryResolveSilently(gen);
6699
+ if (existing)
6700
+ return existing;
6701
+ this.onSignaturePrompt?.();
5813
6702
  const payload = await buildSignedLoginPayload(
5814
6703
  this.signer,
5815
6704
  this.chainId,
5816
6705
  this.serviceEndpoint
5817
6706
  );
5818
- const res = await this.fetchImpl(
5819
- `${this.serviceEndpoint}/api/auth/login`,
5820
- {
5821
- method: "POST",
5822
- headers: { "Content-Type": "application/json" },
5823
- body: JSON.stringify(payload)
5824
- }
5825
- );
5826
- if (!res.ok) {
5827
- const detail = await res.text().catch(() => "");
5828
- throw new Error(
5829
- `lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`
5830
- );
6707
+ const session = await this.login(payload);
6708
+ if (notCleared()) {
6709
+ this.persist(payload, session);
6710
+ this.cached = session;
5831
6711
  }
5832
- const body = await res.json();
5833
- if (!body?.token || !body?.expiresAt) {
5834
- throw new Error("lit-ops-server login returned malformed response");
5835
- }
5836
- this.cached = { token: body.token, expiresAt: body.expiresAt };
5837
- return this.cached;
6712
+ return session;
5838
6713
  } finally {
5839
- this.inFlight = null;
6714
+ if (notCleared())
6715
+ this.inFlight = null;
5840
6716
  }
5841
6717
  })();
5842
6718
  return this.inFlight;
5843
6719
  }
6720
+ async login(payload) {
6721
+ const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/login`, {
6722
+ method: "POST",
6723
+ headers: { "Content-Type": "application/json" },
6724
+ body: JSON.stringify(payload)
6725
+ });
6726
+ if (!res.ok) {
6727
+ const detail = await res.text().catch(() => "");
6728
+ throw new ServerLoginError(
6729
+ `lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`,
6730
+ res.status
6731
+ );
6732
+ }
6733
+ const body = await res.json();
6734
+ if (!body?.token || !body?.expiresAt) {
6735
+ throw new Error("lit-ops-server login returned malformed response");
6736
+ }
6737
+ return { token: body.token, expiresAt: body.expiresAt };
6738
+ }
6739
+ /**
6740
+ * Load the persisted session for the current signer, validating that it
6741
+ * belongs to this address/chain/endpoint and that the envelope is still
6742
+ * inside the re-auth window (with a safety margin). Invalid or expired
6743
+ * entries are evicted.
6744
+ */
6745
+ async loadPersisted() {
6746
+ if (!this.store)
6747
+ return null;
6748
+ try {
6749
+ const address = (await this.signer.getAddress()).toLowerCase();
6750
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6751
+ this.lastStoreKey = key;
6752
+ const persisted = this.store.load(key);
6753
+ if (!persisted)
6754
+ return null;
6755
+ const withinWindow = this.now() < persisted.payload.message.issuedAt + REAUTH_WINDOW_SECONDS - SAFETY_MARGIN_SECONDS;
6756
+ if (persisted.address !== address || persisted.chainId !== this.chainId || persisted.endpoint !== this.serviceEndpoint || persisted.payload.message.address.toLowerCase() !== address || !withinWindow) {
6757
+ this.store.clear(key);
6758
+ return null;
6759
+ }
6760
+ return persisted;
6761
+ } catch {
6762
+ return null;
6763
+ }
6764
+ }
6765
+ /** Best-effort save of the envelope + live JWT for silent recovery. */
6766
+ persist(payload, session) {
6767
+ if (!this.store)
6768
+ return;
6769
+ const address = payload.message.address.toLowerCase();
6770
+ try {
6771
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6772
+ this.lastStoreKey = key;
6773
+ this.store.save(key, {
6774
+ version: 1,
6775
+ address,
6776
+ chainId: this.chainId,
6777
+ endpoint: this.serviceEndpoint,
6778
+ payload,
6779
+ token: session.token,
6780
+ tokenExpiresAt: session.expiresAt
6781
+ });
6782
+ } catch {
6783
+ }
6784
+ }
5844
6785
  };
5845
6786
 
5846
6787
  // src/utils/quantum-revert.utils.ts
@@ -5963,6 +6904,36 @@ async function assertSafeQuantumSubmission(opts) {
5963
6904
  import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
5964
6905
  var QUANTUM_SECONDS = 60;
5965
6906
  var QUANTUM_SAFE_THRESHOLD = 40;
6907
+ function hashBtcInputSet(utxos) {
6908
+ if (!Array.isArray(utxos) || utxos.length === 0) {
6909
+ throw new Error("hashBtcInputSet: at least one input is required");
6910
+ }
6911
+ const normalized = utxos.map((u) => {
6912
+ const txid = String(u.txid).toLowerCase().replace(/^0x/, "");
6913
+ const vout = Number(u.vout);
6914
+ const value = Number(u.value);
6915
+ if (!/^[0-9a-f]{64}$/.test(txid)) {
6916
+ throw new Error(`hashBtcInputSet: invalid txid "${u.txid}"`);
6917
+ }
6918
+ if (!Number.isSafeInteger(vout) || vout < 0) {
6919
+ throw new Error(`hashBtcInputSet: invalid vout ${u.vout} for ${txid}`);
6920
+ }
6921
+ if (!Number.isSafeInteger(value) || value <= 0) {
6922
+ throw new Error(`hashBtcInputSet: invalid value ${u.value} for ${txid}:${vout}`);
6923
+ }
6924
+ return { txid, vout, value };
6925
+ });
6926
+ const seen = /* @__PURE__ */ new Set();
6927
+ for (const u of normalized) {
6928
+ const key = `${u.txid}:${u.vout}`;
6929
+ if (seen.has(key)) {
6930
+ throw new Error(`hashBtcInputSet: duplicate outpoint ${key}`);
6931
+ }
6932
+ seen.add(key);
6933
+ }
6934
+ 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("|");
6935
+ return keccak2563(toUtf8Bytes3(canonical));
6936
+ }
5966
6937
  async function buildBtcExecuteEnvelope(params) {
5967
6938
  const {
5968
6939
  positionId,
@@ -5983,6 +6954,17 @@ async function buildBtcExecuteEnvelope(params) {
5983
6954
  "buildBtcExecuteEnvelope: targetAmount must be a positive number of satoshis"
5984
6955
  );
5985
6956
  }
6957
+ const utxos = params.utxos && params.utxos.length > 0 ? params.utxos : [{ txid, vout, value: satoshis }];
6958
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
6959
+ const authorizedInSet = utxos.some(
6960
+ (u) => normTxid(u.txid) === normTxid(txid) && Number(u.vout) === Number(vout)
6961
+ );
6962
+ if (!authorizedInSet) {
6963
+ throw new Error(
6964
+ `buildBtcExecuteEnvelope: the authorized UTXO ${txid}:${vout} must be part of utxos[]`
6965
+ );
6966
+ }
6967
+ const utxosHash = hashBtcInputSet(utxos);
5986
6968
  const nowSec = Date.now() / 1e3;
5987
6969
  const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
5988
6970
  const secsIntoQuantum = nowSec - currentQuantumStart;
@@ -6019,6 +7001,8 @@ async function buildBtcExecuteEnvelope(params) {
6019
7001
  // targetAddress (BTC destination)
6020
7002
  "uint256",
6021
7003
  // targetAmount (sats authorized to recipient)
7004
+ "bytes32",
7005
+ // utxosHash (canonical hash of the FULL input set — P6/#8)
6022
7006
  "bytes32"
6023
7007
  // actionHash
6024
7008
  ],
@@ -6030,6 +7014,7 @@ async function buildBtcExecuteEnvelope(params) {
6030
7014
  networkFee,
6031
7015
  targetAddress,
6032
7016
  targetAmount,
7017
+ utxosHash,
6033
7018
  actionHash
6034
7019
  ]
6035
7020
  );
@@ -6046,7 +7031,8 @@ async function buildBtcExecuteEnvelope(params) {
6046
7031
  chainId,
6047
7032
  timestamp,
6048
7033
  userSignature,
6049
- borrowerAddress
7034
+ borrowerAddress,
7035
+ utxos
6050
7036
  };
6051
7037
  }
6052
7038
 
@@ -8862,6 +9848,56 @@ var LoanQuery = class {
8862
9848
  })
8863
9849
  );
8864
9850
  }
9851
+ /**
9852
+ * Get loan-event history for every position owned by a wallet.
9853
+ *
9854
+ * Composes getUserPositions + getLoanEvents (the subgraph has no wallet-scoped
9855
+ * event collection). Each returned LoanEvents carries its own positionId, so
9856
+ * callers can flatten and merge into a single per-wallet timeline.
9857
+ *
9858
+ * Bitcoin deposits never appear in the subgraph — merge those in from an
9859
+ * Esplora source on the consumer side.
9860
+ *
9861
+ * @param userAddress - Loan-owner wallet address.
9862
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
9863
+ * @returns One LoanEvents object per position (empty array if the wallet has none).
9864
+ *
9865
+ * @example
9866
+ * ```typescript
9867
+ * const result = await loanQuery.getWalletLoanEvents('0x830b...');
9868
+ * if (result.success) {
9869
+ * const allEvents = result.value; // LoanEvents[] — one per position
9870
+ * }
9871
+ * ```
9872
+ */
9873
+ async getWalletLoanEvents(userAddress, filter) {
9874
+ if (this.config.debug) {
9875
+ log.info("\u{1F50D} Querying wallet loan events", { userAddress, filter });
9876
+ }
9877
+ return tryCatchAsync(
9878
+ async () => {
9879
+ const perPosition = await this.config.graphClient.getWalletLoanEvents(
9880
+ userAddress,
9881
+ filter
9882
+ );
9883
+ if (this.config.debug) {
9884
+ log.info("\u2705 Retrieved wallet loan events", {
9885
+ userAddress,
9886
+ positions: perPosition.length,
9887
+ totalEvents: perPosition.reduce((n, p) => n + p.totalEvents, 0)
9888
+ });
9889
+ }
9890
+ return perPosition;
9891
+ },
9892
+ (error) => new SDKError({
9893
+ message: "Failed to query wallet loan events",
9894
+ category: "SUBGRAPH" /* SUBGRAPH */,
9895
+ severity: "MEDIUM" /* MEDIUM */,
9896
+ originalError: error instanceof Error ? error : new Error(String(error)),
9897
+ context: { userAddress, filter }
9898
+ })
9899
+ );
9900
+ }
8865
9901
  /**
8866
9902
  * Clear loan cache
8867
9903
  *
@@ -11603,6 +12639,39 @@ var DiamondHandsGraph = class {
11603
12639
  totalEvents
11604
12640
  };
11605
12641
  }
12642
+ /**
12643
+ * Get the full loan-event history for every position owned by a wallet.
12644
+ *
12645
+ * The subgraph has no wallet-scoped event collection (`User` only derives
12646
+ * `positions`), so this composes the two existing wallet/position queries:
12647
+ * `getUserPositions(wallet)` to enumerate the wallet's positions, then
12648
+ * `getLoanEvents(positionId, filter)` per position. Each returned `LoanEvents`
12649
+ * already carries its own `positionId`, so callers can flatten and merge into
12650
+ * a single timeline.
12651
+ *
12652
+ * Note: Bitcoin deposits are UTXOs on the Bitcoin network and never appear in
12653
+ * the EVM subgraph — surface those from an Esplora source and merge separately.
12654
+ *
12655
+ * @param userAddress - The loan-owner wallet address.
12656
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
12657
+ * @returns One LoanEvents object per position; empty array if the wallet has none.
12658
+ */
12659
+ async getWalletLoanEvents(userAddress, filter) {
12660
+ const { positions } = await this.getUserPositions(userAddress);
12661
+ const positionIds = positions.map((p) => p?.id != null ? String(p.id) : "").filter((id) => id.length > 0);
12662
+ if (positionIds.length === 0)
12663
+ return [];
12664
+ const CONCURRENCY = 5;
12665
+ const results = [];
12666
+ for (let i = 0; i < positionIds.length; i += CONCURRENCY) {
12667
+ const batch = positionIds.slice(i, i + CONCURRENCY);
12668
+ const settled = await Promise.all(
12669
+ batch.map((positionId) => this.getLoanEvents(positionId, filter))
12670
+ );
12671
+ results.push(...settled);
12672
+ }
12673
+ return results;
12674
+ }
11606
12675
  // ============================================================================
11607
12676
  // Protocol-wide Events
11608
12677
  // ============================================================================
@@ -15019,6 +16088,27 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
15019
16088
  return {};
15020
16089
  return this.serverSession.getAuthHeader();
15021
16090
  }
16091
+ /**
16092
+ * Sign out of the lit-ops-server session for the current auth signer. Call on
16093
+ * wallet disconnect or explicit sign-out so the credential does not outlive
16094
+ * the wallet connection. No-op in standalone mode.
16095
+ *
16096
+ * Attempts a server-side revocation first (`ServerSession.logout()` — which
16097
+ * tombstones the envelope's 24h re-mint window and invalidates the JWT), then
16098
+ * always removes the local credential. If the revocation call fails (offline,
16099
+ * server down, wallet already disconnected) we still clear locally so the
16100
+ * session cannot silently re-mint after sign-out; `logout()` is best-effort
16101
+ * and never prompts the wallet.
16102
+ */
16103
+ async clearServerSession() {
16104
+ if (!this.serverSession)
16105
+ return;
16106
+ try {
16107
+ await this.serverSession.logout();
16108
+ } catch {
16109
+ await this.serverSession.clearPersisted();
16110
+ }
16111
+ }
15022
16112
  /**
15023
16113
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
15024
16114
  * the post-write state. We clear the entire loan-query cache (not just
@@ -15130,7 +16220,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
15130
16220
  this.serverSession = new ServerSession({
15131
16221
  signer: config.authSigner ?? config.contractSigner,
15132
16222
  serviceEndpoint: config.serviceEndpoint,
15133
- chainId: config.chainId
16223
+ chainId: config.chainId,
16224
+ persistSession: config.sessionPersistence?.enabled,
16225
+ sessionStore: config.sessionPersistence?.store,
16226
+ onSignaturePrompt: config.onSessionSignaturePrompt
15134
16227
  });
15135
16228
  }
15136
16229
  const contractManagerResult = createContractManager({
@@ -17771,9 +18864,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
17771
18864
  userSignature: auth.signature,
17772
18865
  rpcUrl: _rpcUrl || this.config.ethRpcUrl,
17773
18866
  // Forward a Bitcoin provider so the validator reaches a reachable esplora on Hardhat
17774
- // (lit-ops maps it to the validator's devBitcoinProviderUrl); ignored off-hardhat. Prefer
18867
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
18868
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
18869
+ // is resolved from the on-chain BitcoinProviderRegistry and this field is server-ignored
18870
+ // — emitting it there only misleads readers into thinking it is caller-controlled. Prefer
17775
18871
  // a per-request override (mirrors the mint path's customBitcoinRpcUrl) over the config default.
17776
- ...(_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
18872
+ ...this.config.chain === "hardhat" && (_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
17777
18873
  bitcoinProviderUrl: _bitcoinRpcUrl || this.config.bitcoinProviders[0].url
17778
18874
  },
17779
18875
  ...this.config.priceProviders && {
@@ -18554,9 +19650,10 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
18554
19650
  rpcUrl: this.config.ethRpcUrl,
18555
19651
  // Pass the SDK's RPC URL to LIT Action
18556
19652
  ...request.rpcUrl && { rpcUrl: request.rpcUrl },
18557
- // Forward the configured Bitcoin provider so the validator reaches a reachable esplora on
18558
- // Hardhat (lit-ops maps it to the validator's devBitcoinProviderUrl); ignored off-hardhat.
18559
- ...this.config.bitcoinProviders?.[0]?.url && {
19653
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
19654
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
19655
+ // comes from the on-chain BitcoinProviderRegistry and this field is server-ignored.
19656
+ ...this.config.chain === "hardhat" && this.config.bitcoinProviders?.[0]?.url && {
18560
19657
  bitcoinProviderUrl: this.config.bitcoinProviders[0].url
18561
19658
  },
18562
19659
  ...this.config.priceProviders && {
@@ -19615,6 +20712,42 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19615
20712
  };
19616
20713
  }
19617
20714
  }
20715
+ /**
20716
+ * TEMPORARY (multi-UTXO withdrawal fix, Phase A): fetch the vault's confirmed
20717
+ * UTXO set from lit-ops-server (`GET /api/lit/vault-utxos`) so the caller can
20718
+ * assemble and sign the FULL input set for a multi-UTXO consolidation
20719
+ * withdrawal. Phase 1 only returns ONE representative UTXO, so a vault whose
20720
+ * balance is spread across several UTXOs otherwise fails the Phase-2 signer
20721
+ * with "Insufficient UTXO value ... across 1 input(s)".
20722
+ *
20723
+ * Returns `[]` on any failure (non-service mode, network error, bad shape) so
20724
+ * callers transparently fall back to the single authorized UTXO. Superseded
20725
+ * by the btc-withdrawal Lit Action returning its own confirmedUTXOs (Phase B).
20726
+ */
20727
+ async fetchConfirmedVaultUtxos(positionId) {
20728
+ if (this.config.mode !== "service" || !this.config.serviceEndpoint)
20729
+ return [];
20730
+ try {
20731
+ const pid = positionId.startsWith("0x") ? positionId : `0x${positionId}`;
20732
+ const resp = await fetch(
20733
+ `${this.config.serviceEndpoint}/api/lit/vault-utxos?positionId=${encodeURIComponent(pid)}`,
20734
+ { method: "GET", headers: { ...await this.getAuthHeader() } }
20735
+ );
20736
+ if (!resp.ok)
20737
+ return [];
20738
+ const json = await resp.json();
20739
+ if (!json.success || !Array.isArray(json.data?.utxos))
20740
+ return [];
20741
+ return json.data.utxos.map((u) => ({
20742
+ txid: String(u.txid),
20743
+ vout: Number(u.vout),
20744
+ value: Number(u.value),
20745
+ confirmations: Number(u.confirmations ?? 0)
20746
+ }));
20747
+ } catch {
20748
+ return [];
20749
+ }
20750
+ }
19618
20751
  /**
19619
20752
  * Execute Bitcoin withdrawal (Phase 2)
19620
20753
  *
@@ -19684,7 +20817,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19684
20817
  targetAmount: request.targetAmount,
19685
20818
  networkFee: request.networkFee,
19686
20819
  chainId,
19687
- signer
20820
+ signer,
20821
+ ...request.utxos ? { utxos: request.utxos } : {}
19688
20822
  });
19689
20823
  const headers = {
19690
20824
  "Content-Type": "application/json",
@@ -19715,7 +20849,10 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19715
20849
  userSignature: envelope.userSignature,
19716
20850
  borrowerAddress: envelope.borrowerAddress,
19717
20851
  chainId: envelope.chainId,
19718
- timestamp: envelope.timestamp
20852
+ timestamp: envelope.timestamp,
20853
+ // P6/#8: the exact input set hashed into the user signature.
20854
+ // The server must forward this set verbatim to the signer.
20855
+ utxos: envelope.utxos
19719
20856
  })
19720
20857
  }
19721
20858
  );
@@ -20055,10 +21192,13 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20055
21192
  }
20056
21193
  },
20057
21194
  rpcUrl,
20058
- ...sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
21195
+ // Hardhat-ONLY: never emitted on prod chains (server-ignored there; provider comes
21196
+ // from the on-chain BitcoinProviderRegistry). The stale-spend action reads
21197
+ // `devBitcoinProviderUrl` directly on Hardhat — this legacy alias is redundant but
21198
+ // kept behind the Hardhat gate so no prod request carries the caller-looking field.
21199
+ ...this.config.chain === "hardhat" && sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
20059
21200
  // Hardhat (chainId 1337/31337) has no on-chain BitcoinProviderRegistry —
20060
- // Lit Actions read `devBitcoinProviderUrl` from globalThis. Always pass;
20061
- // non-hardhat chains ignore it. (Single source of truth: bitcoinProviderUrl.)
21201
+ // Lit Actions read `devBitcoinProviderUrl` from globalThis. Non-hardhat chains ignore it.
20062
21202
  ...sdkBitcoinProviderUrl && { devBitcoinProviderUrl: sdkBitcoinProviderUrl },
20063
21203
  ...params.minConfirmations !== void 0 && {
20064
21204
  minConfirmations: params.minConfirmations
@@ -20224,13 +21364,44 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20224
21364
  if (this.config.debug) {
20225
21365
  log.info("\u{1F50D} Found vault UTXO for execution", { utxoIdentifier });
20226
21366
  }
21367
+ let executionUtxos;
21368
+ const gatheredUtxos = await this.fetchConfirmedVaultUtxos(positionId);
21369
+ if (gatheredUtxos.length > 0) {
21370
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
21371
+ const byOutpoint = /* @__PURE__ */ new Map();
21372
+ for (const u of gatheredUtxos) {
21373
+ const txid = normTxid(u.txid);
21374
+ byOutpoint.set(`${txid}:${u.vout}`, {
21375
+ txid,
21376
+ vout: Number(u.vout),
21377
+ value: Number(u.value)
21378
+ });
21379
+ }
21380
+ const repKey = `${normTxid(authResult.utxoTxid)}:${authResult.utxoVout}`;
21381
+ if (!byOutpoint.has(repKey)) {
21382
+ byOutpoint.set(repKey, {
21383
+ txid: normTxid(authResult.utxoTxid),
21384
+ vout: Number(authResult.utxoVout),
21385
+ value: Number(authResult.utxoSatoshis)
21386
+ });
21387
+ }
21388
+ if (byOutpoint.size > 1) {
21389
+ executionUtxos = Array.from(byOutpoint.values());
21390
+ if (this.config.debug) {
21391
+ log.info("\u{1F9E9} Multi-UTXO withdrawal: signing full input set", {
21392
+ inputs: executionUtxos.length
21393
+ });
21394
+ }
21395
+ }
21396
+ }
20227
21397
  const executionResult = await this.executeBTCWithdrawal({
20228
21398
  positionId,
20229
21399
  utxoIdentifier,
20230
21400
  utxoSatoshis: Number(authResult.utxoSatoshis),
20231
21401
  networkFee,
20232
21402
  destination: withdrawalAddress,
20233
- targetAmount: withdrawalAmount
21403
+ targetAmount: withdrawalAmount,
21404
+ ...executionUtxos ? { utxos: executionUtxos } : {}
20234
21405
  });
20235
21406
  if (this.config.debug) {
20236
21407
  log.info("\u2705 Phase 2 complete: Bitcoin transaction executed", {
@@ -20381,6 +21552,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20381
21552
  async getLoanEvents(positionId, filter) {
20382
21553
  return this.loanQuery.getLoanEvents(positionId, filter);
20383
21554
  }
21555
+ /**
21556
+ * Get loan-event history for every position owned by a wallet.
21557
+ * Composes getUserPositions + getLoanEvents; returns one LoanEvents per
21558
+ * position (each carrying its own positionId). Bitcoin deposits are not on the
21559
+ * subgraph — merge those in from an Esplora source on the consumer side.
21560
+ */
21561
+ async getWalletLoanEvents(userAddress, filter) {
21562
+ return this.loanQuery.getWalletLoanEvents(userAddress, filter);
21563
+ }
20384
21564
  /**
20385
21565
  * Get protocol-wide events across all indexed entity types.
20386
21566
  * Returns a merged, sorted discriminated-union timeline.
@@ -20837,6 +22017,38 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20837
22017
  }));
20838
22018
  }
20839
22019
  }
22020
+ /**
22021
+ * List a vault address's PENDING (not-yet-confirmed) incoming BTC deposits.
22022
+ *
22023
+ * A deposit is "pending" until it reaches the protocol's minimum confirmation
22024
+ * depth (6) — the confirmed balance endpoints (`getVaultBalance` /
22025
+ * `getAddressBalance`) omit anything shallower, so this surfaces in-flight
22026
+ * deposits (0–5 confirmations). Server-delegated: lit-ops-server queries its
22027
+ * configured premium Esplora provider (`/api/lit/pending-deposits`). Requires
22028
+ * service mode.
22029
+ */
22030
+ async getPendingDeposits(vaultAddress) {
22031
+ this.ensureInitialized();
22032
+ if (!isServiceModeConfig(this.config)) {
22033
+ throw new Error("getPendingDeposits requires service mode \u2014 standalone mode is not supported");
22034
+ }
22035
+ const trimmedAddress = vaultAddress?.trim();
22036
+ if (!trimmedAddress) {
22037
+ throw new Error("vaultAddress is required");
22038
+ }
22039
+ const serviceEndpoint = this.config.serviceEndpoint;
22040
+ const url = `${serviceEndpoint}/api/lit/pending-deposits?vaultAddress=${encodeURIComponent(trimmedAddress)}`;
22041
+ const response = await fetch(url, {
22042
+ headers: await this.getAuthHeader()
22043
+ });
22044
+ if (!response.ok) {
22045
+ throw new Error(`Pending deposits request failed: ${response.status} ${response.statusText}`);
22046
+ }
22047
+ const json = await response.json();
22048
+ if (!json.success)
22049
+ throw new Error(json.error ?? "Pending deposits request failed");
22050
+ return json.data ?? [];
22051
+ }
20840
22052
  // ============================================================================
20841
22053
  // HIGH-LEVEL API - Mock Token Operations (Test Networks Only)
20842
22054
  // ============================================================================
@@ -21332,6 +22544,20 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21332
22544
  if (this.config.debug) {
21333
22545
  log.info(`\u{1F310} Calling liquidation server endpoint: ${endpoint}`);
21334
22546
  }
22547
+ const signer = this.getSignerOrThrow();
22548
+ const intentSigner = await signer.getAddress();
22549
+ const chainId = Number(
22550
+ (await this.getProviderOrThrow().getNetwork()).chainId
22551
+ );
22552
+ const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
22553
+ const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
22554
+ const intentTimestamp = Math.floor(Date.now() / 1e3);
22555
+ const intentActionHash = keccak2565(toUtf8Bytes5("liquidate-position"));
22556
+ const intentHash = solidityPackedKeccak2564(
22557
+ ["bytes32", "uint256", "uint256", "address", "bytes32"],
22558
+ [canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
22559
+ );
22560
+ const intentSignature = await signer.signMessage(getBytes4(intentHash));
21335
22561
  const response = await fetch(endpoint, {
21336
22562
  method: "POST",
21337
22563
  headers: { "Content-Type": "application/json" },
@@ -21347,6 +22573,11 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21347
22573
  UCDController: params.contractAddresses.ucdController,
21348
22574
  LiquidationManagerModule: params.contractAddresses.liquidationManager
21349
22575
  },
22576
+ liquidatorIntent: {
22577
+ timestamp: intentTimestamp,
22578
+ chainId,
22579
+ signature: intentSignature
22580
+ },
21350
22581
  rpcUrl: params.rpcUrl || this.config.ethRpcUrl,
21351
22582
  priceProviders: this.config.priceProviders,
21352
22583
  pkpTokenId: params.pkpData.tokenId,