@gvnrdao/dh-sdk 0.0.300 → 0.0.303

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,85 @@ 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
431
+ },
432
+ CollateralManagerModule: {
433
+ previousImplementation: "0x1919860e528Cd2AbbD31a17858c986b8aCc8C185",
434
+ newImplementation: "0x55110B9dd6B2c9f812564e7a1ef42b3c76E1A3Bb",
435
+ upgradedAt: "2026-07-08T20:33:59Z",
436
+ upgradedBy: "AdminModule generic-op 0x740efe0cfe85044479cbd8ee3058aa244057d90703dd2b37044dd6787cbf3485 (3-of-5 ADMIN_ROLE Safes, 24h timelock; executed by 0xfb3B8cbeFB7D666A6Af34C53e189EdC31862a7be in tx 0x430c8e450f12d3d9359cde356ac5e2ef39661d7a89884b5674a596c1090f6cff)",
437
+ reason: "Validator gas fix (#420): BitcoinAddressValidator packed-table lookups; VERSION 1.3.0 -> 1.3.1"
438
+ },
439
+ BitcoinWithdrawalAddressRegistry: {
440
+ previousImplementation: "0x11731bFEC3068F2cc19D016381dfEA3B93013b1C",
441
+ newImplementation: "0x190c08300Ae650fb88ed8156445BBeb13bE9E8a3",
442
+ upgradedAt: "2026-07-08T20:33:59Z",
443
+ upgradedBy: "AdminModule generic-op 0x2603a486c0f34554b864cc1d909b325c7e105331da10ec0fd7e0b9373fa76207 (3-of-5 ADMIN_ROLE Safes, 24h timelock; executed by 0xfb3B8cbeFB7D666A6Af34C53e189EdC31862a7be in tx 0x430c8e450f12d3d9359cde356ac5e2ef39661d7a89884b5674a596c1090f6cff)",
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
425
504
  }
426
505
  },
427
506
  latestEnv: {
@@ -434,9 +513,37 @@ var init_deployment_addresses = __esm({
434
513
  BITCOIN_PROVIDER_REGISTRY: "0x4ab3Fb96e02893Ed6FAa61bad1918390c8DbbFFC",
435
514
  BITCOIN_PROVIDER_REGISTRY_IMPL: "0x7Bc9473E45fb3EA594e8b13992F57D0444902ddB",
436
515
  BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY: "0xf667f77FDba7458345f227eB6Bfe4148243E7270",
437
- BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0x11731bFEC3068F2cc19D016381dfEA3B93013b1C",
516
+ BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0x190c08300Ae650fb88ed8156445BBeb13bE9E8a3",
438
517
  SIMPLE_PSM_V2: "0xfE0C14B4E614f40Bf58845b4B04CeD17fd116532",
439
- 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"
440
547
  }
441
548
  };
442
549
  MAINNET_CONTRACTS = {
@@ -467,44 +574,44 @@ var init_deployment_addresses = __esm({
467
574
  LOCALHOST_DEPLOYMENT = {
468
575
  network: "localhost",
469
576
  chainId: 31337,
470
- timestamp: "2026-03-23T15:33:34.068Z",
577
+ timestamp: "2026-06-11T12:30:59.589Z",
471
578
  deployer: "",
472
579
  contracts: {
473
- MessageHashBuilder: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
474
- UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
580
+ MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
581
+ UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
475
582
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
476
583
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
477
- PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
478
- TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
479
- LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
584
+ PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
585
+ TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
586
+ LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
480
587
  BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
481
588
  CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
482
589
  LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
483
- CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
590
+ CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
484
591
  AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
485
- PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
486
- PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB",
487
- OperationAuthorizationRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0",
488
- PKPValidation: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
592
+ PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
593
+ PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
594
+ OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
595
+ PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
489
596
  }
490
597
  };
491
598
  LOCALHOST_CONTRACTS = {
492
- MessageHashBuilder: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
493
- UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
599
+ MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
600
+ UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
494
601
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
495
602
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
496
- PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
497
- TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
498
- LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
603
+ PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
604
+ TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
605
+ LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
499
606
  BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
500
607
  CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
501
608
  LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
502
- CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
609
+ CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
503
610
  AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
504
- PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
505
- PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB",
506
- OperationAuthorizationRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0",
507
- PKPValidation: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
611
+ PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
612
+ PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
613
+ OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
614
+ PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
508
615
  };
509
616
  ALL_DEPLOYMENTS = {
510
617
  sepolia: SEPOLIA_DEPLOYMENT,
@@ -1196,30 +1303,30 @@ var require_pkg_src = __commonJS({
1196
1303
  }
1197
1304
  },
1198
1305
  btcTransactionSigner: {
1199
- cid: "QmZs56sVTCJv7mgvyRYfpYHFvFLcSeXa4H44qUYT7z14QP",
1306
+ cid: "QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk",
1200
1307
  authorizedCidHex: cidToHex(
1201
- "QmZs56sVTCJv7mgvyRYfpYHFvFLcSeXa4H44qUYT7z14QP"
1308
+ "QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk"
1202
1309
  ),
1203
1310
  name: "Btc Transaction Signer",
1204
1311
  description: "Production Btc Transaction Signer",
1205
- version: "1.0.0",
1312
+ version: "1.1.1",
1206
1313
  deployed: true,
1207
- deployedAt: 1781542204806,
1208
- size: 42376,
1209
- hash: "f4aed3d88f6fbff6623ed2047436cecd4f561a74883171ea6f4ebe0a1129973c"
1314
+ deployedAt: 1784316496778,
1315
+ size: 63679,
1316
+ hash: "b12aa725797af5ef07f8e6945f05741f583d6e479a4670b1121e97c58fe63191"
1210
1317
  },
1211
1318
  ucdMintValidator: {
1212
- cid: "QmPir8F8jmJ4ssQpEC3xFxkEnZxMHcgS6VxBQE1jkTLj5K",
1319
+ cid: "QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW",
1213
1320
  authorizedCidHex: cidToHex(
1214
- "QmPir8F8jmJ4ssQpEC3xFxkEnZxMHcgS6VxBQE1jkTLj5K"
1321
+ "QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW"
1215
1322
  ),
1216
1323
  name: "Ucd Mint Validator",
1217
1324
  description: "Production Ucd Mint Validator",
1218
- version: "1.0.0",
1325
+ version: "1.2.1",
1219
1326
  deployed: true,
1220
- deployedAt: 1781542207366,
1221
- size: 72074,
1222
- hash: "567a51d3beaedbda4f0af2636835ca8a2af89c2264e91f8dc31f522c1a424b7b",
1327
+ deployedAt: 1784316505431,
1328
+ size: 80381,
1329
+ hash: "132a0a8e6e9a1b85d02e00c0e57eaa371d825717255a28c9e644e4b521760026",
1223
1330
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1224
1331
  pkp: {
1225
1332
  publicKey: "0x041ab17cd91fc5c0b761eea6092d032807561b621b82c488826776e04a9158d61ba64d809a4729f0501289d980732cb06d8dfd06999dd9c8efd2f495dad78b31bb",
@@ -1227,17 +1334,17 @@ var require_pkg_src = __commonJS({
1227
1334
  }
1228
1335
  },
1229
1336
  processPaymentValidator: {
1230
- cid: "QmVq19NXJEpMMvaWQdsU5czLoqh28WGWoGgKMSYFaCmfa9",
1337
+ cid: "QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ",
1231
1338
  authorizedCidHex: cidToHex(
1232
- "QmVq19NXJEpMMvaWQdsU5czLoqh28WGWoGgKMSYFaCmfa9"
1339
+ "QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ"
1233
1340
  ),
1234
1341
  name: "Process Payment Validator",
1235
1342
  description: "Production Process Payment Validator",
1236
- version: "1.0.0",
1343
+ version: "1.2.1",
1237
1344
  deployed: true,
1238
- deployedAt: 1781542209526,
1239
- size: 68239,
1240
- hash: "ada27fea57aa3f3b861c49e75ed1f80b72500fdccc1d6e88ee1a3c1396b81867",
1345
+ deployedAt: 1784316520125,
1346
+ size: 76534,
1347
+ hash: "dbdf43d3f1636e0ee883d349ca28f24c5c38ed7e062b76ca75498aaf5fe594ea",
1241
1348
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1242
1349
  pkp: {
1243
1350
  publicKey: "0x04cfccb9b6c7addc79469c0e9fafebef4c70801302c62cdb26b610723a9b8527d23f98a40158c7340d83fd0e62d02b5ec74c12f7a86d98e8c6973b4811110f19ad",
@@ -1245,17 +1352,17 @@ var require_pkg_src = __commonJS({
1245
1352
  }
1246
1353
  },
1247
1354
  extendPositionValidator: {
1248
- cid: "QmP2u3bgKLtfqsFMp6Li72zMY8ZZH3ttAqp9W2wwonUmcb",
1355
+ cid: "QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4",
1249
1356
  authorizedCidHex: cidToHex(
1250
- "QmP2u3bgKLtfqsFMp6Li72zMY8ZZH3ttAqp9W2wwonUmcb"
1357
+ "QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4"
1251
1358
  ),
1252
1359
  name: "Extend Position Validator",
1253
1360
  description: "Production Extend Position Validator",
1254
- version: "1.0.1",
1361
+ version: "1.2.1",
1255
1362
  deployed: true,
1256
- deployedAt: 1781542211640,
1257
- size: 66543,
1258
- hash: "0db666fa2888e9acea13d8e4d79edd4ca252f5a4bb06d97e53aa14a9a34b7143",
1363
+ deployedAt: 1784316534278,
1364
+ size: 74352,
1365
+ hash: "c17bfc9abef16f2372316a14c14281e2acc98afdd1662d095360f1d1c5383f1d",
1259
1366
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1260
1367
  pkp: {
1261
1368
  publicKey: "0x04e52338f6a0c3362800f1a94c4c8f08be5030dde0fa217388c53d1c1a4790f4443c9f2886b2d2d8f4b75a9a156e9b028011b1a1cf7648da0e556aa980ec692836",
@@ -1263,17 +1370,17 @@ var require_pkg_src = __commonJS({
1263
1370
  }
1264
1371
  },
1265
1372
  btcWithdrawal: {
1266
- cid: "QmXtmLdGuEqMKKAtV7d8sHpUV32Kt74na4dRQdpWLasbHJ",
1373
+ cid: "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2",
1267
1374
  authorizedCidHex: cidToHex(
1268
- "QmXtmLdGuEqMKKAtV7d8sHpUV32Kt74na4dRQdpWLasbHJ"
1375
+ "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2"
1269
1376
  ),
1270
1377
  name: "Btc Withdrawal",
1271
1378
  description: "Production Btc Withdrawal",
1272
- version: "1.0.0",
1379
+ version: "1.2.1",
1273
1380
  deployed: true,
1274
- deployedAt: 1781542212709,
1275
- size: 79782,
1276
- hash: "e824eb473df8eba3a025ac3adc46730dbf89f90884d7c05a78cc07b90d6bc4df",
1381
+ deployedAt: 1784316549409,
1382
+ size: 90692,
1383
+ hash: "ca7276763142fe1fc812b70f6319973000faf338a91c531f898f8f6406035c64",
1277
1384
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1278
1385
  pkp: {
1279
1386
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1281,17 +1388,17 @@ var require_pkg_src = __commonJS({
1281
1388
  }
1282
1389
  },
1283
1390
  liquidationValidator: {
1284
- cid: "QmVicBXuZVY8t4DXLPjvXjzLqnBtnogAh5HT7BaL9VeDwF",
1391
+ cid: "QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx",
1285
1392
  authorizedCidHex: cidToHex(
1286
- "QmVicBXuZVY8t4DXLPjvXjzLqnBtnogAh5HT7BaL9VeDwF"
1393
+ "QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx"
1287
1394
  ),
1288
1395
  name: "Liquidation Validator",
1289
1396
  description: "Production Liquidation Validator",
1290
- version: "1.0.0",
1397
+ version: "1.2.1",
1291
1398
  deployed: true,
1292
- deployedAt: 1781542213421,
1293
- size: 58444,
1294
- hash: "0f42e9f8984433fef95875d35eb3c8d985e7ce15ad309e6aa1093d6fd1ba0bb5",
1399
+ deployedAt: 1784316560010,
1400
+ size: 66523,
1401
+ hash: "f74d0163c47d7db04d1547f72c3a9bc1e5b499669089cc6464479a361462ce81",
1295
1402
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1296
1403
  pkp: {
1297
1404
  publicKey: "0x041a241de58976c13e3d7ca5a18e494f7330151e3818706135d50176e78ffb673e58b11f0b67a87c73fade6c6b27d6e7a7377ea9f4c10bf31880f1d725f18cc6f8",
@@ -1317,17 +1424,17 @@ var require_pkg_src = __commonJS({
1317
1424
  }
1318
1425
  },
1319
1426
  loanVaultBtcBalance: {
1320
- cid: "QmUmqL4LcjDSpw2TtuWMcw8xYk8hPxgagY2qHDU8Mx1PLG",
1427
+ cid: "QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu",
1321
1428
  authorizedCidHex: cidToHex(
1322
- "QmUmqL4LcjDSpw2TtuWMcw8xYk8hPxgagY2qHDU8Mx1PLG"
1429
+ "QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu"
1323
1430
  ),
1324
1431
  name: "Loan Vault Btc Balance",
1325
1432
  description: "Production Loan Vault Btc Balance",
1326
- version: "1.0.0",
1433
+ version: "1.2.1",
1327
1434
  deployed: true,
1328
- deployedAt: 1781542215432,
1329
- size: 48397,
1330
- hash: "da3750a70e532b87f3590a3e2d8c2d51e700589f6e111bb291384ab01304af35",
1435
+ deployedAt: 1784316574669,
1436
+ size: 56477,
1437
+ hash: "917535f2911055b2ade2204fd868dd8b7294c7ade23e82aea488aeb0872e35d7",
1331
1438
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1332
1439
  pkp: {
1333
1440
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1341,17 +1448,17 @@ var require_pkg_src = __commonJS({
1341
1448
  // Requires BitcoinProviderRegistry.addAllowedCid(chainId, keccak256(utf8(cid)))
1342
1449
  // on each target chain (the action decrypts registry slots). No OAR entry.
1343
1450
  btcAddressBalance: {
1344
- cid: "QmWRDAg7ZCBFFhGfdXR7aUtQsG5qqBb1yVdLCxh8dPD7G5",
1451
+ cid: "QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA",
1345
1452
  authorizedCidHex: cidToHex(
1346
- "QmWRDAg7ZCBFFhGfdXR7aUtQsG5qqBb1yVdLCxh8dPD7G5"
1453
+ "QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA"
1347
1454
  ),
1348
1455
  name: "Btc Address Balance",
1349
1456
  description: "Address-only signed total BTC balance via registry consensus (no positionId, no price)",
1350
- version: "0.1.0",
1457
+ version: "0.2.1",
1351
1458
  deployed: true,
1352
- deployedAt: 1781542217452,
1353
- size: 25519,
1354
- hash: "000e2f8bdbccdc95b0db0fb3f12412e2e76eea16a8456d37914d324437ae291c",
1459
+ deployedAt: 1784316590403,
1460
+ size: 26986,
1461
+ hash: "49fb14265f3208916f12806df04492900bd08fb1b14d9651ee6278cd9ae4a428",
1355
1462
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1356
1463
  pkp: {
1357
1464
  publicKey: "0x0498cbb31facfa26e3ec83ccdabfe4fe341ca0282e0c3cb81da7113723b9726e1e49efa92165cc3faa66ced4454cea56dbab12267852d6525a55d7c4f128b912cd",
@@ -1385,17 +1492,17 @@ var require_pkg_src = __commonJS({
1385
1492
  hash: "03ee5be239c91c735d2c95db89a0d1b77e602cfc18af8eb64b3281fddcd90a72"
1386
1493
  },
1387
1494
  staleSpendRecoveryValidator: {
1388
- cid: "QmWqJZqoKUqMaGTXe9WUSwdDyeBwXJ9dnR3ioFnVK4RkN5",
1495
+ cid: "QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK",
1389
1496
  authorizedCidHex: cidToHex(
1390
- "QmWqJZqoKUqMaGTXe9WUSwdDyeBwXJ9dnR3ioFnVK4RkN5"
1497
+ "QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK"
1391
1498
  ),
1392
1499
  name: "Stale Spend Recovery Validator",
1393
1500
  description: "Production Stale Spend Recovery Validator",
1394
- version: "1.0.0",
1501
+ version: "1.1.2",
1395
1502
  deployed: true,
1396
- deployedAt: 1781542219438,
1397
- size: 34413,
1398
- hash: "26b11730cf5d9eb4af0b1fe65192d26ccee50369c0472211a0ea11e26ef51cc5",
1503
+ deployedAt: 1784316604502,
1504
+ size: 36785,
1505
+ hash: "28bfe44970541fefe3bae8bd54f25e8775d7cc61b5832a40ba1c18587b6b6358",
1399
1506
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1400
1507
  pkp: {
1401
1508
  publicKey: "0x046da0ad6da7dd4a0063cbeac6f2bdb6a9889c2178b23470833356a8ee5630f7f0eee0db348d9d9aaaadd23eccf0063c8f68ee5e446abe30b882bf1de4c932bc21",
@@ -2146,7 +2253,7 @@ ${errorReport}`);
2146
2253
  }
2147
2254
  init_debug_logger();
2148
2255
  init_session_signature_cache();
2149
- var import_ethers15 = __require("ethers");
2256
+ var import_ethers16 = __require("ethers");
2150
2257
  var EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS = 11e3;
2151
2258
  var GRACE_PERIOD_DAYS = 30;
2152
2259
  var SATOSHIS_PER_BITCOIN = 100000000n;
@@ -2183,6 +2290,20 @@ ${errorReport}`);
2183
2290
  /* REGTEST */
2184
2291
  // Local development
2185
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
+ };
2186
2307
  function validateChain(chain) {
2187
2308
  const normalized = chain.toLowerCase().trim();
2188
2309
  if (normalized === "sepolia") {
@@ -2479,6 +2600,27 @@ ${errorReport}`);
2479
2600
  function createProtocolParametersModule(config) {
2480
2601
  return new ProtocolParametersModule(config);
2481
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
+ }
2482
2624
  var VaultSnapshotModule = class {
2483
2625
  constructor(config) {
2484
2626
  this.config = config;
@@ -2586,112 +2728,11 @@ ${errorReport}`);
2586
2728
  timestamp: Date.now()
2587
2729
  };
2588
2730
  }
2589
- /**
2590
- * Fast vault snapshot using balance method only (no UTXO validation)
2591
- * This is optimized for speed and should be used when UTXO details are not needed.
2592
- *
2593
- * WARNING: This method skips UTXO validation. Use ONLY for non-critical operations.
2594
- * For authorization/security operations, ALWAYS use getVaultSnapshot().
2595
- *
2596
- * @param positionId - Position to snapshot
2597
- * @returns Complete VaultSnapshot with all metrics (using fast balance method)
2598
- */
2599
- async getVaultSnapshotFast(positionId) {
2600
- const positionState = await this.queryPositionState(positionId);
2601
- console.log(
2602
- `[Vault Snapshot] Raw vault address from contract: "${positionState.vaultAddress}"`
2603
- );
2604
- const useStubbedBtcData = this.config.debugOverrides?.useStubbedBtcData === true;
2605
- const [balanceResult, btcPriceUsd] = useStubbedBtcData ? this.buildStubbedBtcSnapshotInputs(positionState) : await Promise.all([
2606
- this.config.vaultBalance.calculateTrustedBalance(
2607
- positionId,
2608
- positionState.vaultAddress,
2609
- true
2610
- // Always skip UTXO validation for fast path
2611
- ),
2612
- this.config.priceOracle.getBTCPrice()
2613
- ]);
2614
- const protocolParams = createProtocolParametersModule({
2615
- termManagerAddress: this.config.termManagerAddress,
2616
- loanOpsManagerAddress: this.config.loanOpsManagerAddress,
2617
- liquidationManagerAddress: this.config.liquidationManagerAddress,
2618
- chain: this.config.chain,
2619
- chainId: this.config.chainId || 1,
2620
- // Default to mainnet if not provided
2621
- rpcUrl: this.config.rpcUrl
2622
- });
2623
- const termForFees = this.config.termOverride ?? positionState.selectedTerm;
2624
- const [liquidationThresholdBps, maxEscalatedThresholdBps, termFees] = await Promise.all([
2625
- protocolParams.getLiquidationThreshold(),
2626
- protocolParams.getMaxEscalatedThreshold(),
2627
- protocolParams.getTermFees(termForFees)
2628
- ]);
2629
- const termStatus = calculateTermStatus(
2630
- positionState.termStartTimestamp,
2631
- positionState.selectedTerm
2632
- );
2633
- const currentLiquidationThreshold = calculateLiquidationThreshold(
2634
- termStatus.isExpired,
2635
- termStatus.daysIntoGracePeriod,
2636
- liquidationThresholdBps,
2637
- maxEscalatedThresholdBps
2638
- );
2639
- const collateralMetrics = calculateCollateralMetrics(
2640
- balanceResult.availableBalance,
2641
- btcPriceUsd,
2642
- positionState.ucdDebt
2643
- );
2644
- const isLiquidatable = collateralMetrics.collateralRatioBps < currentLiquidationThreshold;
2645
- const marginToLiquidationBps = collateralMetrics.collateralRatioBps - currentLiquidationThreshold;
2646
- return {
2647
- // Raw state
2648
- positionId: positionState.positionId,
2649
- pkpId: positionState.pkpId,
2650
- borrower: positionState.borrower,
2651
- vaultAddress: positionState.vaultAddress,
2652
- ucdDebt: positionState.ucdDebt,
2653
- termStartTimestamp: positionState.termStartTimestamp,
2654
- selectedTerm: positionState.selectedTerm,
2655
- status: positionState.status,
2656
- // Loan status enum value (0-7)
2657
- expiryAt: positionState.expiryAt,
2658
- // Loan expiry timestamp
2659
- previousExpiryAt: positionState.previousExpiryAt,
2660
- // expiryAt before last renewal; 0 if never renewed
2661
- totalTerm: positionState.totalTerm,
2662
- // total accumulated loan months; 0 before first renewal
2663
- lastRenewedAt: positionState.lastRenewedAt,
2664
- // timestamp of most recent renewal; createdAt at origination; 0 for pre-H-5 positions
2665
- // Bitcoin state
2666
- totalBTCSats: balanceResult.totalBalance,
2667
- totalUTXOs: balanceResult.totalUTXOs,
2668
- authorizedSpendsSats: balanceResult.authorizedBalance,
2669
- authorizedSpendsHash: balanceResult.authorizedSpendsHash,
2670
- availableBTCSats: balanceResult.availableBalance,
2671
- availableUTXOs: balanceResult.availableUTXOs,
2672
- // Price data
2673
- btcPriceUsd,
2674
- // Calculated metrics
2675
- collateralValueUsd: collateralMetrics.collateralValueUsd,
2676
- collateralRatioBps: collateralMetrics.collateralRatioBps,
2677
- // Term status
2678
- termDurationDays: termStatus.termDurationDays,
2679
- termLengthDays: termStatus.termLengthDays,
2680
- isExpired: termStatus.isExpired,
2681
- daysUntilExpiry: termStatus.daysUntilExpiry,
2682
- daysIntoGracePeriod: termStatus.daysIntoGracePeriod,
2683
- // Liquidation risk
2684
- currentLiquidationThreshold,
2685
- isLiquidatable,
2686
- marginToLiquidationBps,
2687
- // Protocol parameters (from smart contracts)
2688
- liquidationThresholdBps,
2689
- originationFeeBps: termFees.originationFeeBps,
2690
- extensionFeeBps: termFees.extensionFeeBps,
2691
- // Metadata
2692
- timestamp: Date.now()
2693
- };
2694
- }
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.
2695
2736
  buildStubbedBtcSnapshotInputs(positionState) {
2696
2737
  const stubbedAvailableBtcSats = BigInt(
2697
2738
  this.config.debugOverrides?.stubbedAvailableBtcSats ?? "10000000"
@@ -2820,14 +2861,20 @@ ${errorReport}`);
2820
2861
  expiryAt,
2821
2862
  selectedTermMonths
2822
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
+ }
2823
2873
  return {
2824
2874
  positionId: position.positionId,
2825
2875
  pkpId: position.pkpId,
2826
2876
  borrower: position.borrower,
2827
- vaultAddress: this.parseVaultAddress(
2828
- position.vaultAddress,
2829
- validateChain(this.config.chain)
2830
- ),
2877
+ vaultAddress: parsedVaultAddress,
2831
2878
  ucdDebt: BigInt(position.ucdDebt.toString()),
2832
2879
  termStartTimestamp,
2833
2880
  selectedTerm: selectedTermMonths,
@@ -2838,36 +2885,8 @@ ${errorReport}`);
2838
2885
  lastRenewedAt
2839
2886
  };
2840
2887
  }
2841
- /**
2842
- * Parse vault address based on format and network
2843
- *
2844
- * Supports two formats:
2845
- * 1. Single string: "mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"
2846
- * 2. JSON object: {"mainnet":"bc1q...","testnet":"mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"}
2847
- *
2848
- * @param rawVaultAddress Raw vault address from contract
2849
- * @param chain EVM chain (sepolia, ethereum)
2850
- * @returns Parsed Bitcoin address for the appropriate network
2851
- */
2852
- parseVaultAddress(rawVaultAddress, chain) {
2853
- try {
2854
- const parsed = JSON.parse(rawVaultAddress);
2855
- if (typeof parsed === "object" && parsed !== null) {
2856
- const chainLower = chain.toLowerCase();
2857
- if (chainLower === "sepolia") {
2858
- return parsed.regtest || parsed.testnet || parsed.mainnet;
2859
- }
2860
- const bitcoinNetwork = getBitcoinNetworkForChain(chain);
2861
- if (bitcoinNetwork === "testnet") {
2862
- return parsed.testnet || parsed.regtest || parsed.mainnet;
2863
- } else {
2864
- return parsed.mainnet;
2865
- }
2866
- }
2867
- } catch (e) {
2868
- }
2869
- return rawVaultAddress;
2870
- }
2888
+ // parseVaultAddress moved to `modules/vault-address.ts` (shared with the
2889
+ // phase-2 signer for the P6/#7b vault-identity binding).
2871
2890
  /**
2872
2891
  * Quick check if position is liquidatable
2873
2892
  *
@@ -3205,6 +3224,13 @@ ${auth.clientId}`;
3205
3224
  return {};
3206
3225
  }
3207
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
+ }
3208
3234
  var BitcoinDataProvider = class {
3209
3235
  constructor(config) {
3210
3236
  this.config = config;
@@ -3351,13 +3377,16 @@ ${auth.clientId}`;
3351
3377
  address
3352
3378
  );
3353
3379
  const utxos = unspentOutputs.map(
3354
- (utxo) => ({
3355
- txid: utxo.txid,
3356
- vout: utxo.vout,
3357
- satoshis: BigInt(Math.round(utxo.amount * 1e8)),
3358
- // BTC to satoshis as bigint
3359
- confirmations: utxo.confirmations
3360
- })
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
+ }
3361
3390
  );
3362
3391
  return utxos;
3363
3392
  }
@@ -3399,6 +3428,7 @@ ${auth.clientId}`;
3399
3428
  "Invalid UTXO row: missing txid, vout/n, or value/satoshis"
3400
3429
  );
3401
3430
  }
3431
+ assertSaneSatoshis(satRaw, `parseBlockstreamUTXOs ${txid}:${vout}`);
3402
3432
  utxos.push({
3403
3433
  txid,
3404
3434
  vout,
@@ -3577,7 +3607,10 @@ ${auth.clientId}`;
3577
3607
  `Invalid response: missing required fields. funded_txo_sum: ${fundedTxoSum}, spent_txo_sum: ${spentTxoSum}`
3578
3608
  );
3579
3609
  }
3610
+ assertSaneSatoshis(fundedTxoSum, `getBalance ${cleanAddress} funded_txo_sum`);
3611
+ assertSaneSatoshis(spentTxoSum, `getBalance ${cleanAddress} spent_txo_sum`);
3580
3612
  const confirmedBalance = fundedTxoSum - spentTxoSum;
3613
+ assertSaneSatoshis(confirmedBalance, `getBalance ${cleanAddress} confirmed balance`);
3581
3614
  const balance = BigInt(confirmedBalance);
3582
3615
  this.logger.log("3", `Balance: ${balance.toString()} sats`);
3583
3616
  this.logger.log(
@@ -4056,20 +4089,590 @@ ${auth.clientId}`;
4056
4089
  return priceWith8Decimals;
4057
4090
  }
4058
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
+ };
4059
4655
  var VaultBalanceModule = class {
4060
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
+ }
4061
4662
  this.config = config;
4062
4663
  this.bitcoinProvider = config.bitcoinProvider;
4063
4664
  }
4064
4665
  /**
4065
- * Calculate balance (fast path - no UTXO validation)
4666
+ * Calculate balance (arithmetic only - no UTXO validation)
4066
4667
  *
4067
- * Fast balance-only calculation that skips UTXO validation.
4068
- * Uses simple arithmetic: Total Balance - Authorized Balance = Available Balance
4668
+ * Balance-only calculation: Total Balance - Authorized Balance = Available.
4069
4669
  *
4070
- * Use this when you only need the balance amount and don't need:
4071
- * - Individual UTXO details
4072
- * - 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.
4073
4676
  *
4074
4677
  * @param positionId - The position ID to check
4075
4678
  * @param vaultAddress - Bitcoin address of the vault
@@ -4116,17 +4719,18 @@ ${auth.clientId}`;
4116
4719
  * 1. Authorized transaction not found on network (not broadcasted)
4117
4720
  * 2. Authorized transaction has insufficient confirmations (network updating)
4118
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
+ *
4119
4727
  * @param positionId - The position ID to check
4120
4728
  * @param vaultAddress - Bitcoin address of the vault
4121
- * @param skipUTXOValidation - If true, use fast balance-only path (skips UTXO validation)
4122
4729
  * @returns Complete breakdown of vault balance
4123
4730
  * @throws Error if authorized spends are in invalid state
4124
4731
  */
4125
- async calculateTrustedBalance(positionId, vaultAddress, skipUTXOValidation) {
4126
- if (skipUTXOValidation) {
4127
- return await this.calculateBalance(positionId, vaultAddress);
4128
- }
4129
- const minConfirmations = this.config.minConfirmations || 6;
4732
+ async calculateTrustedBalance(positionId, vaultAddress) {
4733
+ const minConfirmations = this.config.minConfirmations;
4130
4734
  const utxoSet = await this.bitcoinProvider.getUTXOSet(
4131
4735
  vaultAddress,
4132
4736
  minConfirmations
@@ -4146,8 +4750,25 @@ ${auth.clientId}`;
4146
4750
  (sum, spend) => sum + spend.satoshis,
4147
4751
  0n
4148
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
+ );
4149
4770
  const availableUTXOs = utxoSet.utxos.filter(
4150
- (utxo) => !this.isUTXOAuthorized(utxo, authorizedSpends)
4771
+ (utxo) => (confirmedConsensusKeys.has(`${utxo.txid}:${utxo.vout}`) || selfChangeKeys.has(`${utxo.txid}:${utxo.vout}`)) && !this.isUTXOAuthorized(utxo, authorizedSpends)
4151
4772
  );
4152
4773
  const availableBalance = availableUTXOs.reduce(
4153
4774
  (sum, utxo) => sum + utxo.satoshis,
@@ -4379,14 +5000,84 @@ ${auth.clientId}`;
4379
5000
  function createVaultBalanceModule(config) {
4380
5001
  return new VaultBalanceModule(config);
4381
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
+ }
4382
5073
  async function executeVaultSnapshot(params) {
4383
5074
  global.ethers = {
4384
- ...import_ethers15.ethers,
5075
+ ...import_ethers16.ethers,
4385
5076
  providers: {
4386
- StaticJsonRpcProvider: import_ethers15.ethers.JsonRpcProvider
5077
+ StaticJsonRpcProvider: import_ethers16.ethers.JsonRpcProvider
4387
5078
  },
4388
- Contract: import_ethers15.ethers.Contract,
4389
- utils: import_ethers15.ethers
5079
+ Contract: import_ethers16.ethers.Contract,
5080
+ utils: import_ethers16.ethers
4390
5081
  // v6 moved utils to top level
4391
5082
  };
4392
5083
  global.Lit = {
@@ -4423,7 +5114,7 @@ ${auth.clientId}`;
4423
5114
  // Default to mainnet if not provided
4424
5115
  rpcUrl: params.rpcUrl,
4425
5116
  bitcoinProvider,
4426
- minConfirmations: params.minConfirmations || 6
5117
+ minConfirmations: params.minConfirmations ?? getChainPolicy(params.chain).minBitcoinBalanceConfirmations
4427
5118
  });
4428
5119
  const vaultSnapshot = createVaultSnapshotModule({
4429
5120
  contractAddress: params.contractAddress,
@@ -4657,7 +5348,8 @@ function getSepoliaConfig() {
4657
5348
  contractVersionRegistry: SEPOLIA_CONTRACTS.ContractVersionRegistry || "",
4658
5349
  feeRecipientRegistry: SEPOLIA_CONTRACTS.FeeRecipientRegistry || "",
4659
5350
  positionDelegateRegistry: SEPOLIA_CONTRACTS.PositionDelegateRegistry || "",
4660
- bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
5351
+ bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
5352
+ carryAgentRegistry: SEPOLIA_CONTRACTS.CarryAgentRegistry || ""
4661
5353
  },
4662
5354
  subgraphs: {
4663
5355
  // Lazy getter for the same module-init snapshot reason as serviceEndpoint.
@@ -4697,7 +5389,8 @@ function getMainnetConfig() {
4697
5389
  contractVersionRegistry: MAINNET_CONTRACTS.ContractVersionRegistry || "",
4698
5390
  feeRecipientRegistry: MAINNET_CONTRACTS.FeeRecipientRegistry || "",
4699
5391
  positionDelegateRegistry: MAINNET_CONTRACTS.PositionDelegateRegistry || "",
4700
- bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
5392
+ bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
5393
+ carryAgentRegistry: MAINNET_CONTRACTS.CarryAgentRegistry || ""
4701
5394
  },
4702
5395
  subgraphs: {
4703
5396
  diamondHandsUrl: "https://gateway-arbitrum.network.thegraph.com/api/subgraphs/id/CCTPsdYqco2jChDLLBQTbdJWwoukVoMt1cXeR9ti6r9A"
@@ -4837,7 +5530,7 @@ var init_network_configs = __esm({
4837
5530
  });
4838
5531
 
4839
5532
  // src/modules/diamond-hands-sdk.ts
4840
- import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder, 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";
4841
5534
 
4842
5535
  // src/types/result.ts
4843
5536
  function success(value) {
@@ -5221,22 +5914,12 @@ var SDKError = class _SDKError extends Error {
5221
5914
 
5222
5915
  // src/utils/chunks/eip1559-broadcast.utils.ts
5223
5916
  import { parseUnits } from "ethers";
5224
- var MINT_UCD_GAS_CEILING = BigInt(2e6);
5917
+ var MINT_UCD_GAS_CEILING = BigInt(13e5);
5225
5918
  var WITHDRAW_BTC_GAS_CEILING = BigInt(3e6);
5226
5919
  var MAKE_PAYMENT_GAS_CEILING = BigInt(1e6);
5227
5920
  var EXTEND_POSITION_GAS_CEILING = BigInt(1e6);
5228
5921
  var LIQUIDATION_COMMIT_GAS_CEILING = BigInt(1e6);
5229
5922
  var LIQUIDATION_REVEAL_GAS_CEILING = BigInt(2e6);
5230
- var MINT_UCD_ESTIMATE_MARGIN_BPS = 2500;
5231
- async function estimateContractCallGasWithMargin(provider, from, to, data, marginBps, ceiling) {
5232
- try {
5233
- const estimated = await provider.estimateGas({ from, to, data });
5234
- const withMargin = estimated * BigInt(1e4 + marginBps) / 10000n;
5235
- return withMargin > ceiling ? ceiling : withMargin;
5236
- } catch {
5237
- return null;
5238
- }
5239
- }
5240
5923
  async function resolveEip1559FeeFields(provider) {
5241
5924
  const feeData = await provider.getFeeData();
5242
5925
  let maxFeePerGas = feeData.maxFeePerGas ?? void 0;
@@ -5744,21 +6427,117 @@ async function buildSignedLoginPayload(signer, chainId, audience) {
5744
6427
  return { chainId, message, signature };
5745
6428
  }
5746
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
+
5747
6507
  // src/utils/server-session.ts
5748
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
+ }
5749
6519
  var ServerSession = class {
5750
6520
  signer;
5751
6521
  serviceEndpoint;
5752
6522
  chainId;
6523
+ store;
6524
+ onSignaturePrompt;
5753
6525
  now;
5754
6526
  fetchImpl;
5755
6527
  cached = null;
5756
6528
  inFlight = null;
6529
+ /**
6530
+ * Store key for the current signer, memoized on first use so the sync
6531
+ * `clear()` can drop the persisted token without an async address lookup.
6532
+ */
6533
+ lastStoreKey = null;
5757
6534
  constructor(opts) {
5758
6535
  assertSafeServiceEndpoint(opts.serviceEndpoint);
5759
6536
  this.signer = opts.signer;
5760
6537
  this.serviceEndpoint = opts.serviceEndpoint.replace(/\/+$/, "");
5761
6538
  this.chainId = opts.chainId;
6539
+ this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6540
+ this.onSignaturePrompt = opts.onSignaturePrompt;
5762
6541
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
5763
6542
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
5764
6543
  }
@@ -5771,19 +6550,56 @@ var ServerSession = class {
5771
6550
  async getAuthHeader() {
5772
6551
  return { Authorization: `Bearer ${await this.getValidToken()}` };
5773
6552
  }
5774
- /** Drop the cached token (e.g. on 401 from server). Next call re-logs in. */
6553
+ /**
6554
+ * Drop the cached token (e.g. on 401 from server). The persisted envelope is
6555
+ * kept — the next call re-mints silently — but its stored token copy is
6556
+ * dropped too so a dead JWT can't be re-adopted from the store.
6557
+ */
5775
6558
  clear() {
5776
6559
  this.cached = null;
5777
6560
  this.inFlight = null;
6561
+ if (!this.store || !this.lastStoreKey)
6562
+ return;
6563
+ const persisted = this.store.load(this.lastStoreKey);
6564
+ if (persisted?.token) {
6565
+ this.store.save(this.lastStoreKey, {
6566
+ ...persisted,
6567
+ token: void 0,
6568
+ tokenExpiresAt: void 0
6569
+ });
6570
+ }
6571
+ }
6572
+ /**
6573
+ * Drop the session AND the persisted re-mint envelope (wallet disconnect /
6574
+ * explicit sign-out paths — anything that must remove the 24h credential
6575
+ * from storage).
6576
+ */
6577
+ async clearPersisted() {
6578
+ this.cached = null;
6579
+ this.inFlight = null;
6580
+ if (!this.store)
6581
+ return;
6582
+ if (this.lastStoreKey) {
6583
+ this.store.clear(this.lastStoreKey);
6584
+ return;
6585
+ }
6586
+ try {
6587
+ const address = (await this.signer.getAddress()).toLowerCase();
6588
+ this.store.clear(
6589
+ sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6590
+ );
6591
+ } catch {
6592
+ }
5778
6593
  }
5779
6594
  /**
5780
- * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`).
5781
- * Clears the local cache regardless of server response so the client stops
5782
- * presenting the token.
6595
+ * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`)
6596
+ * — which also tombstones the envelope's re-mint window server-side — and
6597
+ * remove the persisted credential locally. Clears the local cache regardless
6598
+ * of server response so the client stops presenting the token.
5783
6599
  */
5784
6600
  async logout() {
5785
6601
  const token = this.cached?.token ?? await this.getValidToken().catch(() => null);
5786
- this.clear();
6602
+ await this.clearPersisted();
5787
6603
  if (!token)
5788
6604
  return;
5789
6605
  const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/logout`, {
@@ -5806,41 +6622,116 @@ var ServerSession = class {
5806
6622
  return this.inFlight;
5807
6623
  this.inFlight = (async () => {
5808
6624
  try {
6625
+ const persisted = await this.loadPersisted();
6626
+ if (persisted) {
6627
+ if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6628
+ this.cached = {
6629
+ token: persisted.token,
6630
+ expiresAt: persisted.tokenExpiresAt
6631
+ };
6632
+ return this.cached;
6633
+ }
6634
+ try {
6635
+ const session2 = await this.login(persisted.payload);
6636
+ this.persist(persisted.payload, session2);
6637
+ this.cached = session2;
6638
+ return session2;
6639
+ } catch (error) {
6640
+ if (!isLoginRejection(error))
6641
+ throw error;
6642
+ if (this.store && this.lastStoreKey) {
6643
+ this.store.clear(this.lastStoreKey);
6644
+ }
6645
+ }
6646
+ }
6647
+ this.onSignaturePrompt?.();
5809
6648
  const payload = await buildSignedLoginPayload(
5810
6649
  this.signer,
5811
6650
  this.chainId,
5812
6651
  this.serviceEndpoint
5813
6652
  );
5814
- const res = await this.fetchImpl(
5815
- `${this.serviceEndpoint}/api/auth/login`,
5816
- {
5817
- method: "POST",
5818
- headers: { "Content-Type": "application/json" },
5819
- body: JSON.stringify(payload)
5820
- }
5821
- );
5822
- if (!res.ok) {
5823
- const detail = await res.text().catch(() => "");
5824
- throw new Error(
5825
- `lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`
5826
- );
5827
- }
5828
- const body = await res.json();
5829
- if (!body?.token || !body?.expiresAt) {
5830
- throw new Error("lit-ops-server login returned malformed response");
5831
- }
5832
- this.cached = { token: body.token, expiresAt: body.expiresAt };
5833
- return this.cached;
6653
+ const session = await this.login(payload);
6654
+ this.persist(payload, session);
6655
+ this.cached = session;
6656
+ return session;
5834
6657
  } finally {
5835
6658
  this.inFlight = null;
5836
6659
  }
5837
6660
  })();
5838
6661
  return this.inFlight;
5839
6662
  }
6663
+ async login(payload) {
6664
+ const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/login`, {
6665
+ method: "POST",
6666
+ headers: { "Content-Type": "application/json" },
6667
+ body: JSON.stringify(payload)
6668
+ });
6669
+ if (!res.ok) {
6670
+ const detail = await res.text().catch(() => "");
6671
+ throw new ServerLoginError(
6672
+ `lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`,
6673
+ res.status
6674
+ );
6675
+ }
6676
+ const body = await res.json();
6677
+ if (!body?.token || !body?.expiresAt) {
6678
+ throw new Error("lit-ops-server login returned malformed response");
6679
+ }
6680
+ return { token: body.token, expiresAt: body.expiresAt };
6681
+ }
6682
+ /**
6683
+ * Load the persisted session for the current signer, validating that it
6684
+ * belongs to this address/chain/endpoint and that the envelope is still
6685
+ * inside the re-auth window (with a safety margin). Invalid or expired
6686
+ * entries are evicted.
6687
+ */
6688
+ async loadPersisted() {
6689
+ if (!this.store)
6690
+ return null;
6691
+ try {
6692
+ const address = (await this.signer.getAddress()).toLowerCase();
6693
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6694
+ this.lastStoreKey = key;
6695
+ const persisted = this.store.load(key);
6696
+ if (!persisted)
6697
+ return null;
6698
+ const withinWindow = this.now() < persisted.payload.message.issuedAt + REAUTH_WINDOW_SECONDS - SAFETY_MARGIN_SECONDS;
6699
+ if (persisted.address !== address || persisted.chainId !== this.chainId || persisted.endpoint !== this.serviceEndpoint || persisted.payload.message.address.toLowerCase() !== address || !withinWindow) {
6700
+ this.store.clear(key);
6701
+ return null;
6702
+ }
6703
+ return persisted;
6704
+ } catch {
6705
+ return null;
6706
+ }
6707
+ }
6708
+ /** Best-effort save of the envelope + live JWT for silent recovery. */
6709
+ persist(payload, session) {
6710
+ if (!this.store)
6711
+ return;
6712
+ const address = payload.message.address.toLowerCase();
6713
+ try {
6714
+ const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
6715
+ this.lastStoreKey = key;
6716
+ this.store.save(key, {
6717
+ version: 1,
6718
+ address,
6719
+ chainId: this.chainId,
6720
+ endpoint: this.serviceEndpoint,
6721
+ payload,
6722
+ token: session.token,
6723
+ tokenExpiresAt: session.expiresAt
6724
+ });
6725
+ } catch {
6726
+ }
6727
+ }
5840
6728
  };
5841
6729
 
5842
6730
  // src/utils/quantum-revert.utils.ts
6731
+ import { AbiCoder } from "ethers";
5843
6732
  var DEAD_ZONE_VIOLATION_SELECTOR = "0xbe4b82c1";
6733
+ var ERROR_STRING_SELECTOR = "0x08c379a0";
6734
+ var PANIC_SELECTOR = "0x4e487b71";
5844
6735
  var QUANTUM_REVERT_NAMES = {
5845
6736
  "0xbe4b82c1": "DeadZoneViolation()",
5846
6737
  "0x137f3b70": "InDeadZone()",
@@ -5864,14 +6755,128 @@ function decodeQuantumRevert(e) {
5864
6755
  e?.error?.data?.data ?? e?.error?.data ?? e?.error?.error?.data;
5865
6756
  const data = typeof raw === "string" && raw.startsWith("0x") ? raw : null;
5866
6757
  const selector = data && data.length >= 10 ? data.slice(0, 10) : null;
5867
- const name = selector ? QUANTUM_REVERT_NAMES[selector] ?? null : null;
6758
+ let name = selector ? QUANTUM_REVERT_NAMES[selector] ?? null : null;
6759
+ if (!name && selector && data) {
6760
+ try {
6761
+ if (selector === ERROR_STRING_SELECTOR) {
6762
+ const [message] = AbiCoder.defaultAbiCoder().decode(
6763
+ ["string"],
6764
+ "0x" + data.slice(10)
6765
+ );
6766
+ name = `Error("${message}")`;
6767
+ } else if (selector === PANIC_SELECTOR) {
6768
+ const [code] = AbiCoder.defaultAbiCoder().decode(
6769
+ ["uint256"],
6770
+ "0x" + data.slice(10)
6771
+ );
6772
+ name = `Panic(0x${code.toString(16)})`;
6773
+ }
6774
+ } catch {
6775
+ }
6776
+ }
5868
6777
  return { data, selector, name };
5869
6778
  }
5870
6779
 
6780
+ // src/utils/quantum-submission.utils.ts
6781
+ var MAX_DEADZONE_RESIMULATIONS = 3;
6782
+ var QuantumRevertError = class extends Error {
6783
+ constructor(errorName, selector, data, cause) {
6784
+ super(
6785
+ `Pre-send simulation reverted: ${errorName ?? selector ?? "unknown"}`
6786
+ );
6787
+ this.errorName = errorName;
6788
+ this.selector = selector;
6789
+ this.data = data;
6790
+ this.cause = cause;
6791
+ this.name = "QuantumRevertError";
6792
+ }
6793
+ };
6794
+ async function assertSafeQuantumSubmission(opts) {
6795
+ const { provider, to, from, data, quantumTimestamp } = opts;
6796
+ const maxResimulations = opts.maxResimulations ?? MAX_DEADZONE_RESIMULATIONS;
6797
+ const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6798
+ const onDebug = opts.onDebug ?? (() => {
6799
+ });
6800
+ const gateOpts = {
6801
+ now,
6802
+ ...opts.sleep ? { sleep: opts.sleep } : {}
6803
+ };
6804
+ await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
6805
+ for (let sim = 1; ; sim++) {
6806
+ try {
6807
+ await provider.call({ to, from, data });
6808
+ break;
6809
+ } catch (simError) {
6810
+ const decoded = decodeQuantumRevert(simError);
6811
+ if (decoded.selector === DEAD_ZONE_VIOLATION_SELECTOR && sim <= maxResimulations) {
6812
+ const sigQuantum = Math.floor(quantumTimestamp / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
6813
+ const realCurrentQuantum = Math.floor(now() / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
6814
+ if (sigQuantum === realCurrentQuantum) {
6815
+ onDebug(
6816
+ "\u21AA\uFE0F Simulated DeadZoneViolation is a stale-latest-block artifact (signature is the current quantum) \u2014 proceeding to broadcast"
6817
+ );
6818
+ break;
6819
+ }
6820
+ const gate = await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
6821
+ if (gate.waited) {
6822
+ onDebug(
6823
+ `\u{1F501} Re-simulating after crossing the quantum boundary (attempt ${sim}/${maxResimulations})`
6824
+ );
6825
+ continue;
6826
+ }
6827
+ }
6828
+ if (!decoded.data) {
6829
+ const label = simError instanceof Error ? simError.message : String(simError);
6830
+ onDebug(
6831
+ `\u26A0\uFE0F Pre-send simulation failed without revert data (${label}) \u2014 proceeding to broadcast`
6832
+ );
6833
+ break;
6834
+ }
6835
+ throw new QuantumRevertError(
6836
+ decoded.name,
6837
+ decoded.selector,
6838
+ decoded.data,
6839
+ simError
6840
+ );
6841
+ }
6842
+ }
6843
+ await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
6844
+ }
6845
+
5871
6846
  // src/utils/btc-withdrawal-message.ts
5872
6847
  import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
5873
6848
  var QUANTUM_SECONDS = 60;
5874
6849
  var QUANTUM_SAFE_THRESHOLD = 40;
6850
+ function hashBtcInputSet(utxos) {
6851
+ if (!Array.isArray(utxos) || utxos.length === 0) {
6852
+ throw new Error("hashBtcInputSet: at least one input is required");
6853
+ }
6854
+ const normalized = utxos.map((u) => {
6855
+ const txid = String(u.txid).toLowerCase().replace(/^0x/, "");
6856
+ const vout = Number(u.vout);
6857
+ const value = Number(u.value);
6858
+ if (!/^[0-9a-f]{64}$/.test(txid)) {
6859
+ throw new Error(`hashBtcInputSet: invalid txid "${u.txid}"`);
6860
+ }
6861
+ if (!Number.isSafeInteger(vout) || vout < 0) {
6862
+ throw new Error(`hashBtcInputSet: invalid vout ${u.vout} for ${txid}`);
6863
+ }
6864
+ if (!Number.isSafeInteger(value) || value <= 0) {
6865
+ throw new Error(`hashBtcInputSet: invalid value ${u.value} for ${txid}:${vout}`);
6866
+ }
6867
+ return { txid, vout, value };
6868
+ });
6869
+ const seen = /* @__PURE__ */ new Set();
6870
+ for (const u of normalized) {
6871
+ const key = `${u.txid}:${u.vout}`;
6872
+ if (seen.has(key)) {
6873
+ throw new Error(`hashBtcInputSet: duplicate outpoint ${key}`);
6874
+ }
6875
+ seen.add(key);
6876
+ }
6877
+ 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("|");
6878
+ return keccak2563(toUtf8Bytes3(canonical));
6879
+ }
5875
6880
  async function buildBtcExecuteEnvelope(params) {
5876
6881
  const {
5877
6882
  positionId,
@@ -5892,6 +6897,17 @@ async function buildBtcExecuteEnvelope(params) {
5892
6897
  "buildBtcExecuteEnvelope: targetAmount must be a positive number of satoshis"
5893
6898
  );
5894
6899
  }
6900
+ const utxos = params.utxos && params.utxos.length > 0 ? params.utxos : [{ txid, vout, value: satoshis }];
6901
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
6902
+ const authorizedInSet = utxos.some(
6903
+ (u) => normTxid(u.txid) === normTxid(txid) && Number(u.vout) === Number(vout)
6904
+ );
6905
+ if (!authorizedInSet) {
6906
+ throw new Error(
6907
+ `buildBtcExecuteEnvelope: the authorized UTXO ${txid}:${vout} must be part of utxos[]`
6908
+ );
6909
+ }
6910
+ const utxosHash = hashBtcInputSet(utxos);
5895
6911
  const nowSec = Date.now() / 1e3;
5896
6912
  const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
5897
6913
  const secsIntoQuantum = nowSec - currentQuantumStart;
@@ -5928,6 +6944,8 @@ async function buildBtcExecuteEnvelope(params) {
5928
6944
  // targetAddress (BTC destination)
5929
6945
  "uint256",
5930
6946
  // targetAmount (sats authorized to recipient)
6947
+ "bytes32",
6948
+ // utxosHash (canonical hash of the FULL input set — P6/#8)
5931
6949
  "bytes32"
5932
6950
  // actionHash
5933
6951
  ],
@@ -5939,6 +6957,7 @@ async function buildBtcExecuteEnvelope(params) {
5939
6957
  networkFee,
5940
6958
  targetAddress,
5941
6959
  targetAmount,
6960
+ utxosHash,
5942
6961
  actionHash
5943
6962
  ]
5944
6963
  );
@@ -5955,7 +6974,8 @@ async function buildBtcExecuteEnvelope(params) {
5955
6974
  chainId,
5956
6975
  timestamp,
5957
6976
  userSignature,
5958
- borrowerAddress
6977
+ borrowerAddress,
6978
+ utxos
5959
6979
  };
5960
6980
  }
5961
6981
 
@@ -8771,6 +9791,56 @@ var LoanQuery = class {
8771
9791
  })
8772
9792
  );
8773
9793
  }
9794
+ /**
9795
+ * Get loan-event history for every position owned by a wallet.
9796
+ *
9797
+ * Composes getUserPositions + getLoanEvents (the subgraph has no wallet-scoped
9798
+ * event collection). Each returned LoanEvents carries its own positionId, so
9799
+ * callers can flatten and merge into a single per-wallet timeline.
9800
+ *
9801
+ * Bitcoin deposits never appear in the subgraph — merge those in from an
9802
+ * Esplora source on the consumer side.
9803
+ *
9804
+ * @param userAddress - Loan-owner wallet address.
9805
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
9806
+ * @returns One LoanEvents object per position (empty array if the wallet has none).
9807
+ *
9808
+ * @example
9809
+ * ```typescript
9810
+ * const result = await loanQuery.getWalletLoanEvents('0x830b...');
9811
+ * if (result.success) {
9812
+ * const allEvents = result.value; // LoanEvents[] — one per position
9813
+ * }
9814
+ * ```
9815
+ */
9816
+ async getWalletLoanEvents(userAddress, filter) {
9817
+ if (this.config.debug) {
9818
+ log.info("\u{1F50D} Querying wallet loan events", { userAddress, filter });
9819
+ }
9820
+ return tryCatchAsync(
9821
+ async () => {
9822
+ const perPosition = await this.config.graphClient.getWalletLoanEvents(
9823
+ userAddress,
9824
+ filter
9825
+ );
9826
+ if (this.config.debug) {
9827
+ log.info("\u2705 Retrieved wallet loan events", {
9828
+ userAddress,
9829
+ positions: perPosition.length,
9830
+ totalEvents: perPosition.reduce((n, p) => n + p.totalEvents, 0)
9831
+ });
9832
+ }
9833
+ return perPosition;
9834
+ },
9835
+ (error) => new SDKError({
9836
+ message: "Failed to query wallet loan events",
9837
+ category: "SUBGRAPH" /* SUBGRAPH */,
9838
+ severity: "MEDIUM" /* MEDIUM */,
9839
+ originalError: error instanceof Error ? error : new Error(String(error)),
9840
+ context: { userAddress, filter }
9841
+ })
9842
+ );
9843
+ }
8774
9844
  /**
8775
9845
  * Clear loan cache
8776
9846
  *
@@ -11512,6 +12582,39 @@ var DiamondHandsGraph = class {
11512
12582
  totalEvents
11513
12583
  };
11514
12584
  }
12585
+ /**
12586
+ * Get the full loan-event history for every position owned by a wallet.
12587
+ *
12588
+ * The subgraph has no wallet-scoped event collection (`User` only derives
12589
+ * `positions`), so this composes the two existing wallet/position queries:
12590
+ * `getUserPositions(wallet)` to enumerate the wallet's positions, then
12591
+ * `getLoanEvents(positionId, filter)` per position. Each returned `LoanEvents`
12592
+ * already carries its own `positionId`, so callers can flatten and merge into
12593
+ * a single timeline.
12594
+ *
12595
+ * Note: Bitcoin deposits are UTXOs on the Bitcoin network and never appear in
12596
+ * the EVM subgraph — surface those from an Esplora source and merge separately.
12597
+ *
12598
+ * @param userAddress - The loan-owner wallet address.
12599
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
12600
+ * @returns One LoanEvents object per position; empty array if the wallet has none.
12601
+ */
12602
+ async getWalletLoanEvents(userAddress, filter) {
12603
+ const { positions } = await this.getUserPositions(userAddress);
12604
+ const positionIds = positions.map((p) => p?.id != null ? String(p.id) : "").filter((id) => id.length > 0);
12605
+ if (positionIds.length === 0)
12606
+ return [];
12607
+ const CONCURRENCY = 5;
12608
+ const results = [];
12609
+ for (let i = 0; i < positionIds.length; i += CONCURRENCY) {
12610
+ const batch = positionIds.slice(i, i + CONCURRENCY);
12611
+ const settled = await Promise.all(
12612
+ batch.map((positionId) => this.getLoanEvents(positionId, filter))
12613
+ );
12614
+ results.push(...settled);
12615
+ }
12616
+ return results;
12617
+ }
11515
12618
  // ============================================================================
11516
12619
  // Protocol-wide Events
11517
12620
  // ============================================================================
@@ -14928,6 +16031,15 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
14928
16031
  return {};
14929
16032
  return this.serverSession.getAuthHeader();
14930
16033
  }
16034
+ /**
16035
+ * Remove the persisted lit-ops-server session (the 24h re-mint envelope and
16036
+ * its JWT) for the current auth signer. Call on wallet disconnect or
16037
+ * explicit sign-out so the credential does not outlive the wallet
16038
+ * connection. No-op in standalone mode.
16039
+ */
16040
+ async clearServerSession() {
16041
+ await this.serverSession?.clearPersisted();
16042
+ }
14931
16043
  /**
14932
16044
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
14933
16045
  * the post-write state. We clear the entire loan-query cache (not just
@@ -15039,7 +16151,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
15039
16151
  this.serverSession = new ServerSession({
15040
16152
  signer: config.authSigner ?? config.contractSigner,
15041
16153
  serviceEndpoint: config.serviceEndpoint,
15042
- chainId: config.chainId
16154
+ chainId: config.chainId,
16155
+ persistSession: config.sessionPersistence?.enabled,
16156
+ sessionStore: config.sessionPersistence?.store,
16157
+ onSignaturePrompt: config.onSessionSignaturePrompt
15043
16158
  });
15044
16159
  }
15045
16160
  const contractManagerResult = createContractManager({
@@ -16223,8 +17338,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16223
17338
  throw new Error(`Position not found: ${request.positionId}`);
16224
17339
  }
16225
17340
  const currentDebt = position.ucdDebt.toString();
16226
- const ucdDebtHash = keccak2565(AbiCoder.defaultAbiCoder().encode(["uint256"], [currentDebt]));
16227
- const contractHash = keccak2565(AbiCoder.defaultAbiCoder().encode(
17341
+ const ucdDebtHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
17342
+ const contractHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(
16228
17343
  ["address", "address", "address", "address"],
16229
17344
  [
16230
17345
  positionManagerAddress,
@@ -16235,263 +17350,55 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16235
17350
  ));
16236
17351
  if (this.config.debug) {
16237
17352
  log.info("\u{1F50D} DEBUG: Detailed Contract Call Parameters", {});
16238
- log.info(" === LIT Action Response ===", {});
16239
- log.info(
16240
- ` authorizedSpendsHash: ${validationResponse.authorizedSpendsHash}`,
16241
- {}
16242
- );
16243
- log.info(` mintAmount: ${validationResponse.mintAmount}`, {});
16244
- log.info(` mintFee: ${validationResponse.mintFee}`, {});
16245
- log.info(` newDebt: ${validationResponse.newDebt}`, {});
16246
- log.info(` newCollateral: ${validationResponse.newCollateral}`, {});
16247
- log.info(` btcPrice: ${validationResponse.btcPrice}`, {});
16248
- log.info(
16249
- ` timestamp (quantumTimestamp): ${validationResponse.timestamp}`,
16250
- {}
16251
- );
16252
- log.info(" === SDK Calculated Hashes ===", {});
16253
- log.info(` Current UCD Debt (from position): ${currentDebt}`, {});
16254
- log.info(` ucdDebtHash: ${ucdDebtHash}`, {});
16255
- log.info(" === Contract Addresses for contractHash ===", {});
16256
- log.info(` PositionManager: ${positionManagerAddress}`, {});
16257
- log.info(
16258
- ` LoanOpsManager: ${this.getContractAddressesOrThrow().loanOperationsManager}`,
16259
- {}
16260
- );
16261
- log.info(
16262
- ` TermManager: ${this.getContractAddressesOrThrow().termManager}`,
16263
- {}
16264
- );
16265
- log.info(
16266
- ` UCDController: ${this.getContractAddressesOrThrow().ucdController}`,
16267
- {}
16268
- );
16269
- log.info(` contractHash: ${contractHash}`, {});
16270
- log.info(" === Signature ===", {});
16271
- log.info(
16272
- ` Signature (first 20 chars): ${finalSignature.substring(
16273
- 0,
16274
- 20
16275
- )}...`,
16276
- {}
16277
- );
16278
- log.info(` Signature length: ${finalSignature.length}`, {});
16279
- log.info(
16280
- ` Final signature (first 50 chars): ${finalSignature ? finalSignature.substring(0, 50) : "EMPTY"}`,
16281
- {}
16282
- );
16283
- log.info(
16284
- ` Final signature is empty: ${!finalSignature || finalSignature.length === 0}`,
16285
- {}
16286
- );
16287
- }
16288
- if (this.config.debug) {
16289
- log.info("\u{1F50D} PREFLIGHT: Attempting diagnostic gas estimation...", {});
16290
- }
16291
- try {
16292
- const iface = new Interface4(positionManagerAbi);
16293
- const signatureHex = finalSignature.startsWith("0x") ? finalSignature : "0x" + finalSignature;
16294
- if (this.config.debug) {
16295
- log.info("\u{1F50D} DEBUG: Signature details:", {
16296
- finalSignature: finalSignature ? `${finalSignature.substring(0, 20)}... (length: ${finalSignature.length})` : "UNDEFINED/EMPTY",
16297
- signatureHex: signatureHex ? `${signatureHex.substring(0, 22)}... (length: ${signatureHex.length})` : "UNDEFINED/EMPTY"
16298
- });
16299
- }
16300
- const calldata = iface.encodeFunctionData("mintUCD", [
16301
- positionIdBytes32,
16302
- validationResponse.mintAmount,
16303
- validationResponse.mintFee,
16304
- validationResponse.newDebt,
16305
- validationResponse.newCollateral,
16306
- validationResponse.btcPrice,
16307
- validationResponse.authorizedSpendsHash,
16308
- ucdDebtHash,
16309
- contractHash,
16310
- validationResponse.timestamp,
16311
- signatureHex
16312
- ]);
16313
- if (this.config.debug) {
16314
- log.info("\u{1F50D} DEBUG: Encoded calldata:", {
16315
- length: calldata.length,
16316
- signaturePortion: calldata.substring(650, 670)
16317
- });
16318
- }
16319
- const provider = this.getSignerOrThrow().provider;
16320
- if (!provider) {
16321
- throw new Error("Provider not available");
16322
- }
16323
- const signerAddress = await this.getSignerOrThrow().getAddress();
16324
- if (this.config.debug) {
16325
- log.info(" Executing diagnostic eth_call...", {});
16326
- }
16327
- await provider.call({
16328
- to: positionManagerAddress,
16329
- from: signerAddress,
16330
- data: calldata
16331
- });
16332
- if (this.config.debug) {
16333
- log.info(" \u2705 Diagnostic eth_call succeeded!", {});
16334
- }
16335
- } catch (preflightError) {
16336
- if (this.config.debug) {
16337
- log.error(
16338
- "\u274C PREFLIGHT CHECK FAILED - Diagnostic eth_call reverted",
16339
- {}
16340
- );
16341
- log.error(` Error message: ${preflightError?.message}`, {});
16342
- log.error(` Error code: ${preflightError?.code}`, {});
16343
- log.error(` Error data: ${preflightError?.data || "none"}`, {});
16344
- log.error(` Error reason: ${preflightError?.reason || "none"}`, {});
16345
- log.error(
16346
- ` Error.error: ${JSON.stringify(preflightError?.error || {})}`,
16347
- {}
16348
- );
16349
- }
16350
- let errorData = preflightError?.data || preflightError?.info?.error?.data || preflightError?.error?.data?.data || preflightError?.error?.data || preflightError?.error?.error?.data;
16351
- const errorMessage = preflightError?.reason || preflightError?.error?.reason || preflightError?.error?.error?.reason || preflightError?.message || "Unknown error";
16352
- let requireMessage;
16353
- if (errorData && typeof errorData === "string" && errorData.startsWith("0x") && errorData.length > 138) {
16354
- try {
16355
- const errorSelector = errorData.slice(0, 10);
16356
- if (errorSelector === "0x08c379a0") {
16357
- const iface = new Interface4([
16358
- "function Error(string)"
16359
- ]);
16360
- const decoded = iface.decodeFunctionData("Error", errorData);
16361
- requireMessage = decoded[0];
16362
- }
16363
- } catch (e) {
16364
- }
16365
- }
16366
- const isDeadZonePreflightArtifact = decodeQuantumRevert(preflightError).selector === DEAD_ZONE_VIOLATION_SELECTOR;
16367
- if (isDeadZonePreflightArtifact) {
16368
- const nowSec = Math.floor(Date.now() / 1e3);
16369
- const sigQuantum2 = Math.floor(Number(validationResponse.timestamp) / 60) * 60;
16370
- const realCurrentQuantum = Math.floor(nowSec / 60) * 60;
16371
- if (sigQuantum2 === realCurrentQuantum) {
16372
- if (this.config.debug) {
16373
- log.info(
16374
- "\u21AA\uFE0F Preflight DeadZoneViolation is a stale-latest-block artifact (signature is the current quantum) \u2014 proceeding to broadcast",
16375
- {}
16376
- );
16377
- }
16378
- } else {
16379
- if (this.config.debug) {
16380
- log.info(
16381
- "\u{1F501} Preflight DeadZoneViolation near the boundary \u2014 deferring across it, then proceeding",
16382
- {}
16383
- );
16384
- }
16385
- await awaitSafeSubmissionWindow(
16386
- Number(validationResponse.timestamp)
16387
- );
16388
- }
16389
- }
16390
- if (requireMessage && !isDeadZonePreflightArtifact) {
16391
- throw new Error(
16392
- `Preflight check failed - mintUCD would revert:
16393
- ${requireMessage}
16394
- Context: Quantum timestamp=${validationResponse.timestamp}, Position=${request.positionId}`
16395
- );
16396
- }
16397
- if (errorData && typeof errorData === "string" && errorData.startsWith("0x") && errorData.length > 10) {
16398
- const selector = errorData.slice(0, 10);
16399
- const knownErrors = {
16400
- "0x8baa579f": "InvalidSignature()",
16401
- "0xd92e233d": "ValidationFailed()",
16402
- "0x3ee5aeb5": "OperationNotAuthorized()",
16403
- "0x70f4b32a": "UnauthorizedSigner()",
16404
- "0x48f5c3ed": "Unauthorized()",
16405
- "0x36bfdeea": "StateHashMismatch()",
16406
- "0x131d9a21": "QuantumExpired()",
16407
- "0x52ce5d58": "QuantumAlreadyUsed()",
16408
- "0x137f3b70": "InDeadZone()",
16409
- "0xbe4b82c1": "DeadZoneViolation()",
16410
- "0x3e76a3c9": "QuantumOutsideWindow()",
16411
- "0x62278171": "InvalidValidatorSignature()",
16412
- "0xb9d419a7": "DebtUpdateVerificationFailed()",
16413
- "0xc7e20553": "DebtUpdateVerificationFailedDetailed(bytes32,uint256,uint256,uint256,uint256,uint256)",
16414
- "0x0cfd2a97": "MintVerificationMismatch(uint256,uint256)",
16415
- "0x07637bd8": "MintFailed()"
16416
- };
16417
- const errorName = knownErrors[selector] || `Unknown error ${selector}`;
16418
- const mintDebtDiagnostics = selector === "0xb9d419a7" || selector === "0xc7e20553" || selector === "0x0cfd2a97" || selector === "0x07637bd8" ? `
16419
-
16420
- Mint/debt diagnostics (LoanOperationsManager mint path \u2192 increaseDebtFromMint):
16421
- positionId: ${request.positionId}
16422
- on-chain ucdDebt before tx: ${currentDebt}
16423
- Lit newDebt: ${validationResponse.newDebt}
16424
- Lit mintAmount: ${validationResponse.mintAmount}
16425
- Lit mintFee: ${validationResponse.mintFee}
16426
- Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
16427
- Contract expects UCD supply increase to match mintAmount+mintFee and debt update to newDebt.
16428
- ` : "";
16429
- let quantumContext = "";
16430
- if (selector === "0x131d9a21" || selector === "0x52ce5d58" || selector === "0x137f3b70" || selector === "0xbe4b82c1" || selector === "0x3e76a3c9") {
16431
- const currentTime2 = Math.floor(Date.now() / 1e3);
16432
- const currentQuantum = Math.floor(currentTime2 / 60) * 60;
16433
- const sigQuantum2 = Math.floor(validationResponse.timestamp / 60) * 60;
16434
- const timeDiff = currentTime2 - validationResponse.timestamp;
16435
- quantumContext = `
16436
-
16437
- Quantum Timing Analysis:
16438
- Signature created: ${validationResponse.timestamp} (${new Date(
16439
- validationResponse.timestamp * 1e3
16440
- ).toISOString()})
16441
- Current time: ${currentTime2} (${new Date(
16442
- currentTime2 * 1e3
16443
- ).toISOString()})
16444
- Time elapsed: ${timeDiff} seconds
16445
- Signature quantum: ${sigQuantum2}
16446
- Current quantum: ${currentQuantum}
16447
- Quantum window: 60 seconds
16448
- Status: ${sigQuantum2 < currentQuantum ? "\u274C EXPIRED - Signature created in older quantum window" : "\u2705 VALID"}
16449
- Note: QuantumOutsideWindow can also occur if the tx is mined outside the accepted past/current/next quantum set.`;
16450
- }
16451
- if (!isDeadZonePreflightArtifact) {
16452
- throw new Error(
16453
- `Preflight check failed - mintUCD would revert:
16454
- Error: ${errorName}
16455
- Selector: ${selector}
16456
- Message: ${errorMessage}
16457
- Data: ${errorData}${quantumContext}${mintDebtDiagnostics}`
16458
- );
16459
- }
16460
- } else {
16461
- const currentTime2 = Math.floor(Date.now() / 1e3);
16462
- const timeSinceSig = currentTime2 - validationResponse.timestamp;
16463
- const currentQuantum = Math.floor(currentTime2 / 60) * 60;
16464
- const sigQuantum2 = Math.floor(validationResponse.timestamp / 60) * 60;
16465
- throw new Error(
16466
- `Preflight check failed - mintUCD would revert:
16467
- ${errorMessage}
16468
-
16469
- Error Details:
16470
- Error data: ${errorData || "0x (empty - no custom error)"}
16471
- Error code: ${preflightError?.code || "none"}
16472
-
16473
- Quantum Timing Analysis:
16474
- Signature created: ${validationResponse.timestamp} (${new Date(
16475
- validationResponse.timestamp * 1e3
16476
- ).toISOString()})
16477
- Current time: ${currentTime2} (${new Date(
16478
- currentTime2 * 1e3
16479
- ).toISOString()})
16480
- Time elapsed: ${timeSinceSig} seconds
16481
- Signature quantum: ${sigQuantum2}
16482
- Current quantum: ${currentQuantum}
16483
- Status: ${sigQuantum2 < currentQuantum ? "\u274C EXPIRED - Signature too old" : "\u2705 VALID"}
16484
-
16485
- Possible Causes (when error data is empty):
16486
- \u2022 Quantum signature expired (current=${currentQuantum}, signature=${sigQuantum2})
16487
- \u2022 Contract paused or circuit breaker active
16488
- \u2022 Position state changed since LIT Action signed
16489
- \u2022 Authorization registry validation failed
16490
- \u2022 RPC provider not returning detailed revert data
16491
-
16492
- Position: ${request.positionId}`
16493
- );
16494
- }
17353
+ log.info(" === LIT Action Response ===", {});
17354
+ log.info(
17355
+ ` authorizedSpendsHash: ${validationResponse.authorizedSpendsHash}`,
17356
+ {}
17357
+ );
17358
+ log.info(` mintAmount: ${validationResponse.mintAmount}`, {});
17359
+ log.info(` mintFee: ${validationResponse.mintFee}`, {});
17360
+ log.info(` newDebt: ${validationResponse.newDebt}`, {});
17361
+ log.info(` newCollateral: ${validationResponse.newCollateral}`, {});
17362
+ log.info(` btcPrice: ${validationResponse.btcPrice}`, {});
17363
+ log.info(
17364
+ ` timestamp (quantumTimestamp): ${validationResponse.timestamp}`,
17365
+ {}
17366
+ );
17367
+ log.info(" === SDK Calculated Hashes ===", {});
17368
+ log.info(` Current UCD Debt (from position): ${currentDebt}`, {});
17369
+ log.info(` ucdDebtHash: ${ucdDebtHash}`, {});
17370
+ log.info(" === Contract Addresses for contractHash ===", {});
17371
+ log.info(` PositionManager: ${positionManagerAddress}`, {});
17372
+ log.info(
17373
+ ` LoanOpsManager: ${this.getContractAddressesOrThrow().loanOperationsManager}`,
17374
+ {}
17375
+ );
17376
+ log.info(
17377
+ ` TermManager: ${this.getContractAddressesOrThrow().termManager}`,
17378
+ {}
17379
+ );
17380
+ log.info(
17381
+ ` UCDController: ${this.getContractAddressesOrThrow().ucdController}`,
17382
+ {}
17383
+ );
17384
+ log.info(` contractHash: ${contractHash}`, {});
17385
+ log.info(" === Signature ===", {});
17386
+ log.info(
17387
+ ` Signature (first 20 chars): ${finalSignature.substring(
17388
+ 0,
17389
+ 20
17390
+ )}...`,
17391
+ {}
17392
+ );
17393
+ log.info(` Signature length: ${finalSignature.length}`, {});
17394
+ log.info(
17395
+ ` Final signature (first 50 chars): ${finalSignature ? finalSignature.substring(0, 50) : "EMPTY"}`,
17396
+ {}
17397
+ );
17398
+ log.info(
17399
+ ` Final signature is empty: ${!finalSignature || finalSignature.length === 0}`,
17400
+ {}
17401
+ );
16495
17402
  }
16496
17403
  try {
16497
17404
  const coreAddress = this.getContractAddressesOrThrow().positionManagerCore;
@@ -16510,7 +17417,7 @@ Position: ${request.positionId}`
16510
17417
  positionIdBytes32
16511
17418
  );
16512
17419
  const currentDebtHash = keccak2565(
16513
- AbiCoder.defaultAbiCoder().encode(
17420
+ AbiCoder2.defaultAbiCoder().encode(
16514
17421
  ["uint256"],
16515
17422
  [currentPosition.ucdDebt]
16516
17423
  )
@@ -16688,146 +17595,121 @@ Position: ${request.positionId}`
16688
17595
  signatureHexMint
16689
17596
  ]);
16690
17597
  const fromAddress = await signer.getAddress();
16691
- const estimatedGas = await estimateContractCallGasWithMargin(
16692
- signerProvider,
16693
- fromAddress,
16694
- positionManagerAddress,
16695
- mintCalldata,
16696
- MINT_UCD_ESTIMATE_MARGIN_BPS,
16697
- MINT_UCD_GAS_CEILING
16698
- );
16699
- let tx;
16700
- if (estimatedGas !== null) {
16701
- if (this.config.debug) {
16702
- log.info(
16703
- `\u{1F4DD} Broadcasting mintUCD (gasLimit=${estimatedGas.toString()})`,
16704
- {}
16705
- );
16706
- }
16707
- await awaitSafeSubmissionWindow(Number(validationResponse.timestamp));
16708
- tx = await sendEip1559Transaction({
16709
- signer,
17598
+ try {
17599
+ await assertSafeQuantumSubmission({
17600
+ provider: signerProvider,
16710
17601
  to: positionManagerAddress,
17602
+ from: fromAddress,
16711
17603
  data: mintCalldata,
16712
- gasLimit: estimatedGas
17604
+ quantumTimestamp: Number(validationResponse.timestamp),
17605
+ onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
16713
17606
  });
16714
- } else {
16715
- if (this.config.debug) {
16716
- log.info(
16717
- "\u{1F50D} eth_estimateGas failed for mintUCD; running static call",
16718
- {}
16719
- );
16720
- }
16721
- try {
16722
- await signerProvider.call({
16723
- to: positionManagerAddress,
16724
- from: fromAddress,
16725
- data: mintCalldata
16726
- });
16727
- } catch (callError) {
16728
- if (this.config.debug) {
16729
- log.info("\u{1F50D} DEBUG: mintUCD static call failed", {});
16730
- log.info(` Error message: ${callError?.message}`, {});
16731
- log.info(` Error code: ${callError?.code}`, {});
16732
- log.info(` Error data type: ${typeof callError?.data}`, {});
16733
- log.info(` Error data value: ${callError?.data}`, {});
16734
- }
16735
- let errorData = callError?.data || callError?.info?.error?.data || callError?.error?.data?.data || callError?.error?.data;
16736
- const errorMessage = callError?.reason || callError?.message || "Unknown error";
16737
- const isDeadZoneArtifact = decodeQuantumRevert(callError).selector === DEAD_ZONE_VIOLATION_SELECTOR;
16738
- if (isDeadZoneArtifact && this.config.debug) {
16739
- log.info(
16740
- "\u{1F536} mintUCD static-call DeadZoneViolation is a stale-latest-block artifact; broadcasting anyway (gated)",
16741
- {}
16742
- );
16743
- }
16744
- if (!isDeadZoneArtifact && errorData && typeof errorData === "string" && errorData.startsWith("0x") && errorData.length > 10) {
16745
- const selector = errorData.slice(0, 10);
16746
- const knownErrors = {
16747
- "0x8baa579f": "InvalidSignature()",
16748
- "0xd92e233d": "ValidationFailed()",
16749
- "0x3ee5aeb5": "OperationNotAuthorized()",
16750
- "0x70f4b32a": "UnauthorizedSigner()",
16751
- "0x48f5c3ed": "Unauthorized()",
16752
- "0x36bfdeea": "StateHashMismatch()",
16753
- "0x131d9a21": "QuantumExpired()",
16754
- "0x52ce5d58": "QuantumAlreadyUsed()",
16755
- "0x137f3b70": "InDeadZone()",
16756
- "0xbe4b82c1": "DeadZoneViolation()",
16757
- "0x3e76a3c9": "QuantumOutsideWindow()",
16758
- "0x62278171": "InvalidValidatorSignature()",
16759
- "0xb9d419a7": "DebtUpdateVerificationFailed()",
16760
- "0xc7e20553": "DebtUpdateVerificationFailedDetailed(bytes32,uint256,uint256,uint256,uint256,uint256)",
16761
- "0x0cfd2a97": "MintVerificationMismatch(uint256,uint256)",
16762
- "0x07637bd8": "MintFailed()"
16763
- };
16764
- const errorName = knownErrors[selector] || `Unknown error ${selector}`;
16765
- throw new Error(
16766
- `mintUCD reverted: ${errorName}
16767
- Error selector: ${selector}
16768
- Error data: ${errorData}
16769
- Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
16770
- Message: ${errorMessage}`
16771
- );
16772
- }
16773
- if (errorData === "0x" || !errorData) {
16774
- const viewsAddress = this.config.contractAddresses?.positionManagerViews;
16775
- if (viewsAddress) {
16776
- try {
16777
- const views = new Contract6(
16778
- viewsAddress,
16779
- [
16780
- "function diagnoseMintUCD(bytes32,uint256,uint256,uint256,uint256,uint256,bytes32,bytes32,bytes32,uint256,bytes) view returns (string)"
16781
- ],
16782
- signerProvider
16783
- );
16784
- const diagnosis = await views["diagnoseMintUCD"](
16785
- request.positionId,
16786
- validationResponse.mintAmount,
16787
- validationResponse.mintFee,
16788
- validationResponse.newDebt,
16789
- validationResponse.newCollateral,
16790
- validationResponse.btcPrice,
16791
- validationResponse.authorizedSpendsHash,
16792
- ucdDebtHash,
16793
- contractHash,
16794
- validationResponse.timestamp,
16795
- signatureHexMint
16796
- );
16797
- throw new Error(
16798
- `mintUCD reverted with empty data; diagnosis: ${diagnosis}`
17607
+ } catch (simError) {
17608
+ if (!(simError instanceof QuantumRevertError)) {
17609
+ throw simError;
17610
+ }
17611
+ const simCause = simError.cause;
17612
+ const causeMessage = simCause?.reason ?? simCause?.shortMessage ?? (simCause instanceof Error ? simCause.message : String(simCause));
17613
+ if (!simError.data) {
17614
+ const viewsAddress = this.config.contractAddresses?.positionManagerViews;
17615
+ if (viewsAddress) {
17616
+ let diagnosis = null;
17617
+ try {
17618
+ const views = new Contract6(
17619
+ viewsAddress,
17620
+ [
17621
+ "function diagnoseMintUCD(bytes32,uint256,uint256,uint256,uint256,uint256,bytes32,bytes32,bytes32,uint256,bytes) view returns (string)"
17622
+ ],
17623
+ signerProvider
17624
+ );
17625
+ diagnosis = await views["diagnoseMintUCD"](
17626
+ request.positionId,
17627
+ validationResponse.mintAmount,
17628
+ validationResponse.mintFee,
17629
+ validationResponse.newDebt,
17630
+ validationResponse.newCollateral,
17631
+ validationResponse.btcPrice,
17632
+ validationResponse.authorizedSpendsHash,
17633
+ ucdDebtHash,
17634
+ contractHash,
17635
+ validationResponse.timestamp,
17636
+ signatureHexMint
17637
+ );
17638
+ } catch (diagErr) {
17639
+ if (this.config.debug) {
17640
+ log.info(
17641
+ ` diagnoseMintUCD unavailable or failed: ${diagErr?.message || diagErr}`,
17642
+ {}
16799
17643
  );
16800
- } catch (diagErr) {
16801
- if (this.config.debug) {
16802
- log.info(
16803
- ` diagnoseMintUCD unavailable or failed: ${diagErr?.message || diagErr}`,
16804
- {}
16805
- );
16806
- }
16807
17644
  }
16808
17645
  }
17646
+ if (diagnosis !== null) {
17647
+ throw new Error(
17648
+ `mintUCD reverted with empty data; diagnosis: ${diagnosis}`
17649
+ );
17650
+ }
16809
17651
  }
16810
- if (!isDeadZoneArtifact) {
16811
- throw new Error(
16812
- `mintUCD reverted: ${errorMessage}
16813
- Error data: ${errorData || "none"}`
16814
- );
16815
- }
16816
- }
16817
- if (this.config.debug) {
16818
- log.info(
16819
- "\u2705 Static call clear \u2014 broadcasting mintUCD with fixed gas ceiling",
16820
- {}
17652
+ throw new Error(
17653
+ `mintUCD would revert (pre-send simulation): ${causeMessage}
17654
+ Error data: none`
16821
17655
  );
16822
17656
  }
16823
- await awaitSafeSubmissionWindow(Number(validationResponse.timestamp));
16824
- tx = await sendEip1559Transaction({
16825
- signer,
16826
- to: positionManagerAddress,
16827
- data: mintCalldata,
16828
- gasLimit: MINT_UCD_GAS_CEILING
16829
- });
17657
+ const selector = simError.selector;
17658
+ const errorName = simError.errorName ?? (selector ? `Unknown error ${selector}` : "unknown");
17659
+ const mintDebtDiagnostics = selector === "0xb9d419a7" || // DebtUpdateVerificationFailed()
17660
+ selector === "0xc7e20553" || // DebtUpdateVerificationFailedDetailed(...)
17661
+ selector === "0x0cfd2a97" || // MintVerificationMismatch(uint256,uint256)
17662
+ selector === "0x07637bd8" ? `
17663
+
17664
+ Mint/debt diagnostics (LoanOperationsManager mint path \u2192 increaseDebtFromMint):
17665
+ positionId: ${request.positionId}
17666
+ on-chain ucdDebt before tx: ${currentDebt}
17667
+ Lit newDebt: ${validationResponse.newDebt}
17668
+ Lit mintAmount: ${validationResponse.mintAmount}
17669
+ Lit mintFee: ${validationResponse.mintFee}
17670
+ Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
17671
+ Contract expects UCD supply increase to match mintAmount+mintFee and debt update to newDebt.
17672
+ ` : "";
17673
+ let quantumContext = "";
17674
+ if (selector === "0x131d9a21" || // QuantumExpired()
17675
+ selector === "0x52ce5d58" || // QuantumAlreadyUsed()
17676
+ selector === "0x137f3b70" || // InDeadZone()
17677
+ selector === "0xbe4b82c1" || // DeadZoneViolation()
17678
+ selector === "0x3e76a3c9") {
17679
+ const nowSecs = Math.floor(Date.now() / 1e3);
17680
+ const nowQuantum = Math.floor(nowSecs / 60) * 60;
17681
+ const signedQuantum = Math.floor(Number(validationResponse.timestamp) / 60) * 60;
17682
+ const timeDiff = nowSecs - Number(validationResponse.timestamp);
17683
+ quantumContext = `
17684
+
17685
+ Quantum Timing Analysis:
17686
+ Signature created: ${validationResponse.timestamp} (${new Date(
17687
+ Number(validationResponse.timestamp) * 1e3
17688
+ ).toISOString()})
17689
+ Current time: ${nowSecs} (${new Date(
17690
+ nowSecs * 1e3
17691
+ ).toISOString()})
17692
+ Time elapsed: ${timeDiff} seconds
17693
+ Signature quantum: ${signedQuantum}
17694
+ Current quantum: ${nowQuantum}
17695
+ Quantum window: 60 seconds
17696
+ Status: ${signedQuantum < nowQuantum ? "\u274C EXPIRED - Signature created in older quantum window" : "\u2705 VALID"}
17697
+ Note: QuantumOutsideWindow can also occur if the tx is mined outside the accepted past/current/next quantum set.`;
17698
+ }
17699
+ throw new Error(
17700
+ `mintUCD would revert (pre-send simulation): ${errorName}
17701
+ Error selector: ${selector}
17702
+ Error data: ${simError.data}
17703
+ Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
17704
+ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
17705
+ );
16830
17706
  }
17707
+ const tx = await sendEip1559Transaction({
17708
+ signer,
17709
+ to: positionManagerAddress,
17710
+ data: mintCalldata,
17711
+ gasLimit: MINT_UCD_GAS_CEILING
17712
+ });
16831
17713
  const receipt = await signerProvider.waitForTransaction(
16832
17714
  tx.hash,
16833
17715
  1,
@@ -17913,9 +18795,12 @@ Error data: ${errorData || "none"}`
17913
18795
  userSignature: auth.signature,
17914
18796
  rpcUrl: _rpcUrl || this.config.ethRpcUrl,
17915
18797
  // Forward a Bitcoin provider so the validator reaches a reachable esplora on Hardhat
17916
- // (lit-ops maps it to the validator's devBitcoinProviderUrl); ignored off-hardhat. Prefer
18798
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
18799
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
18800
+ // is resolved from the on-chain BitcoinProviderRegistry and this field is server-ignored
18801
+ // — emitting it there only misleads readers into thinking it is caller-controlled. Prefer
17917
18802
  // a per-request override (mirrors the mint path's customBitcoinRpcUrl) over the config default.
17918
- ...(_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
18803
+ ...this.config.chain === "hardhat" && (_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
17919
18804
  bitcoinProviderUrl: _bitcoinRpcUrl || this.config.bitcoinProviders[0].url
17920
18805
  },
17921
18806
  ...this.config.priceProviders && {
@@ -17991,26 +18876,43 @@ Error data: ${errorData || "none"}`
17991
18876
  if (!signatureBytes.startsWith("0x")) {
17992
18877
  signatureBytes = "0x" + signatureBytes;
17993
18878
  }
17994
- const contractManager = this.getContractManager();
17995
- const positionManagerResult = contractManager.getPositionManager();
17996
- if (!positionManagerResult.success) {
17997
- throw new Error(
17998
- `Failed to get PositionManager: ${positionManagerResult.error.message}`
17999
- );
18000
- }
18001
- const positionManager = positionManagerResult.value;
18002
- await awaitSafeSubmissionWindow(Number(quantumTimestamp));
18003
- const extendPositionFn = positionManager["extendPosition"];
18004
- const tx = await extendPositionFn(
18879
+ const extendIface = new Interface4([
18880
+ "function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, uint256 proRataRenewalFee, bytes calldata extensionValidatorSignature) external returns (bool)"
18881
+ ]);
18882
+ const extendCalldata = extendIface.encodeFunctionData("extendPosition", [
18005
18883
  positionIdBytes32,
18006
18884
  BigInt(_selectedTerm),
18007
18885
  quantumTimestamp,
18008
18886
  btcPrice,
18009
18887
  availableBTCBalance,
18010
18888
  proRataRenewalFee,
18011
- signatureBytes,
18012
- { gasLimit: EXTEND_POSITION_GAS_CEILING }
18013
- );
18889
+ signatureBytes
18890
+ ]);
18891
+ const extendTo = this.getContractAddressesOrThrow().positionManager;
18892
+ const extendSigner = this.getSignerOrThrow();
18893
+ try {
18894
+ await assertSafeQuantumSubmission({
18895
+ provider: this.getProviderOrThrow(),
18896
+ to: extendTo,
18897
+ from: await extendSigner.getAddress(),
18898
+ data: extendCalldata,
18899
+ quantumTimestamp: Number(quantumTimestamp),
18900
+ onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
18901
+ });
18902
+ } catch (simError) {
18903
+ if (!(simError instanceof QuantumRevertError)) {
18904
+ throw simError;
18905
+ }
18906
+ throw new Error(
18907
+ `extendPosition would revert (pre-send simulation): ${simError.errorName ?? simError.selector ?? "unknown"}`
18908
+ );
18909
+ }
18910
+ const tx = await sendEip1559Transaction({
18911
+ signer: extendSigner,
18912
+ to: extendTo,
18913
+ data: extendCalldata,
18914
+ gasLimit: EXTEND_POSITION_GAS_CEILING
18915
+ });
18014
18916
  const txWithProvider = tx;
18015
18917
  if (!txWithProvider.provider) {
18016
18918
  const signer2 = this.getSignerOrThrow();
@@ -18679,9 +19581,10 @@ Error data: ${errorData || "none"}`
18679
19581
  rpcUrl: this.config.ethRpcUrl,
18680
19582
  // Pass the SDK's RPC URL to LIT Action
18681
19583
  ...request.rpcUrl && { rpcUrl: request.rpcUrl },
18682
- // Forward the configured Bitcoin provider so the validator reaches a reachable esplora on
18683
- // Hardhat (lit-ops maps it to the validator's devBitcoinProviderUrl); ignored off-hardhat.
18684
- ...this.config.bitcoinProviders?.[0]?.url && {
19584
+ // Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
19585
+ // validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
19586
+ // comes from the on-chain BitcoinProviderRegistry and this field is server-ignored.
19587
+ ...this.config.chain === "hardhat" && this.config.bitcoinProviders?.[0]?.url && {
18685
19588
  bitcoinProviderUrl: this.config.bitcoinProviders[0].url
18686
19589
  },
18687
19590
  ...this.config.priceProviders && {
@@ -18870,7 +19773,6 @@ Error data: ${errorData || "none"}`
18870
19773
  quantumTimestamp: quantumTimestamp.toString()
18871
19774
  });
18872
19775
  }
18873
- await awaitSafeSubmissionWindow(Number(quantumTimestamp));
18874
19776
  const paymentAmountStr = paymentAmountWei.toString();
18875
19777
  const quantumTimestampStr = quantumTimestamp.toString();
18876
19778
  const btcPriceStr = btcPrice.toString();
@@ -18888,58 +19790,30 @@ Error data: ${errorData || "none"}`
18888
19790
  btcPriceStr,
18889
19791
  signature
18890
19792
  ]);
18891
- const decodePaymentRevert = (e) => {
18892
- const { selector, name } = decodeQuantumRevert(e);
18893
- if (!selector)
18894
- return null;
18895
- return name ?? `Unknown error ${selector}`;
18896
- };
18897
- const MAX_DEADZONE_RESIMULATIONS = 3;
18898
- for (let sim = 1; ; sim++) {
18899
- try {
18900
- await paymentProvider.call({
18901
- to: paymentContractAddress,
18902
- from: paymentFrom,
18903
- data: paymentCalldata
18904
- });
18905
- break;
18906
- } catch (simError) {
18907
- const decoded = decodePaymentRevert(simError);
18908
- if (decoded === "DeadZoneViolation()" && sim <= MAX_DEADZONE_RESIMULATIONS) {
18909
- const nowSec = Math.floor(Date.now() / 1e3);
18910
- const sigQuantum = Math.floor(Number(quantumTimestamp) / 60) * 60;
18911
- const realCurrentQuantum = Math.floor(nowSec / 60) * 60;
18912
- if (sigQuantum === realCurrentQuantum) {
18913
- if (this.config.debug) {
18914
- log.info(
18915
- "\u21AA\uFE0F Simulated DeadZoneViolation is a stale-latest-block artifact (signature is the current quantum) \u2014 proceeding to broadcast",
18916
- {}
18917
- );
18918
- }
18919
- break;
18920
- }
18921
- const gate = await awaitSafeSubmissionWindow(Number(quantumTimestamp));
18922
- if (gate.waited) {
18923
- if (this.config.debug) {
18924
- log.info(
18925
- `\u{1F501} Re-simulating makePayment after crossing the quantum boundary (attempt ${sim}/${MAX_DEADZONE_RESIMULATIONS})`,
18926
- {}
18927
- );
18928
- }
18929
- continue;
18930
- }
18931
- }
18932
- const label = decoded ?? simError?.reason ?? simError?.shortMessage ?? (simError instanceof Error ? simError.message : String(simError));
18933
- if (this.config.debug) {
18934
- log.error("\u274C makePayment pre-send simulation reverted", { error: label });
18935
- }
18936
- return {
18937
- success: false,
18938
- error: `Payment would revert (pre-send simulation): ${label}`,
18939
- positionId: request.positionId,
18940
- paymentAmountUCD: request.paymentAmount
18941
- };
19793
+ try {
19794
+ await assertSafeQuantumSubmission({
19795
+ provider: paymentProvider,
19796
+ to: paymentContractAddress,
19797
+ from: paymentFrom,
19798
+ data: paymentCalldata,
19799
+ quantumTimestamp: Number(quantumTimestamp),
19800
+ onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
19801
+ });
19802
+ } catch (simError) {
19803
+ if (!(simError instanceof QuantumRevertError)) {
19804
+ throw simError;
19805
+ }
19806
+ const simCause = simError.cause;
19807
+ const label = simError.errorName ?? (simError.selector ? `Unknown error ${simError.selector}` : null) ?? simCause?.reason ?? simCause?.shortMessage ?? (simCause instanceof Error ? simCause.message : String(simCause));
19808
+ if (this.config.debug) {
19809
+ log.error("\u274C makePayment pre-send simulation reverted", { error: label });
18942
19810
  }
19811
+ return {
19812
+ success: false,
19813
+ error: `Payment would revert (pre-send simulation): ${label}`,
19814
+ positionId: request.positionId,
19815
+ paymentAmountUCD: request.paymentAmount
19816
+ };
18943
19817
  }
18944
19818
  const tx = await sendEip1559Transaction({
18945
19819
  signer: paymentSigner,
@@ -19613,13 +20487,6 @@ Error data: ${errorData || "none"}`
19613
20487
  error: "PositionManager address not configured"
19614
20488
  };
19615
20489
  }
19616
- const positionManagerContract = new Contract6(
19617
- positionManagerAddress,
19618
- [
19619
- "function withdrawBTC((bytes32 positionId, bytes32 actionHash, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractBundleHash, string withdrawalAddress, uint256 totalDeduction, uint256 newCollateral, uint256 quantumTimestamp, uint256 btcPrice, string utxoTxid, uint32 utxoVout) params, bytes withdrawalValidatorSignature, bytes btcSpendAuthSignature) external returns (bool)"
19620
- ],
19621
- this.getSignerOrThrow()
19622
- );
19623
20490
  const withdrawalParams = {
19624
20491
  positionId: positionIdBytes32,
19625
20492
  actionHash: validationResponse.actionHash,
@@ -19652,86 +20519,56 @@ Error data: ${errorData || "none"}`
19652
20519
  utxoVout: withdrawalParams.utxoVout
19653
20520
  });
19654
20521
  }
19655
- await awaitSafeSubmissionWindow(Number(withdrawalParams.quantumTimestamp));
20522
+ const withdrawIface = new Interface4([
20523
+ "function withdrawBTC((bytes32 positionId, bytes32 actionHash, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractBundleHash, string withdrawalAddress, uint256 totalDeduction, uint256 newCollateral, uint256 quantumTimestamp, uint256 btcPrice, string utxoTxid, uint32 utxoVout) params, bytes withdrawalValidatorSignature, bytes btcSpendAuthSignature) external returns (bool)"
20524
+ ]);
20525
+ const withdrawCalldata = withdrawIface.encodeFunctionData("withdrawBTC", [
20526
+ withdrawalParams,
20527
+ signatureBytes,
20528
+ btcSpendAuthBytes
20529
+ ]);
20530
+ const withdrawSigner = this.getSignerOrThrow();
19656
20531
  let tx;
19657
20532
  try {
19658
- tx = await positionManagerContract["withdrawBTC"](
19659
- withdrawalParams,
19660
- signatureBytes,
19661
- btcSpendAuthBytes,
19662
- { gasLimit: WITHDRAW_BTC_GAS_CEILING }
19663
- );
20533
+ await assertSafeQuantumSubmission({
20534
+ provider: this.getProviderOrThrow(),
20535
+ to: positionManagerAddress,
20536
+ from: await withdrawSigner.getAddress(),
20537
+ data: withdrawCalldata,
20538
+ quantumTimestamp: Number(withdrawalParams.quantumTimestamp),
20539
+ onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
20540
+ });
20541
+ tx = await sendEip1559Transaction({
20542
+ signer: withdrawSigner,
20543
+ to: positionManagerAddress,
20544
+ data: withdrawCalldata,
20545
+ gasLimit: WITHDRAW_BTC_GAS_CEILING
20546
+ });
19664
20547
  } catch (txError) {
19665
- const txErrorMsg = txError instanceof Error ? txError.message : String(txError);
20548
+ const txErrorMsg = txError instanceof QuantumRevertError ? `Withdrawal would revert (pre-send simulation): ${txError.errorName ?? txError.selector ?? "unknown"}` : txError instanceof Error ? txError.message : String(txError);
19666
20549
  if (this.config.debug) {
19667
- log.warn(
19668
- "\u26A0\uFE0F withdrawBTC contract interface failed, falling back to raw transaction",
19669
- { error: txErrorMsg }
19670
- );
20550
+ log.error("\u274C withdrawBTC transaction submission failed", {
20551
+ error: txErrorMsg,
20552
+ withdrawalParams,
20553
+ signatureBytesLength: signatureBytes.length
20554
+ });
19671
20555
  }
19672
20556
  try {
19673
- const contractAddress = this.getContractAddressesOrThrow().positionManager;
19674
- const iface = new Interface4([
19675
- "function withdrawBTC((bytes32 positionId, bytes32 actionHash, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractBundleHash, string withdrawalAddress, uint256 totalDeduction, uint256 newCollateral, uint256 quantumTimestamp, uint256 btcPrice, string utxoTxid, uint32 utxoVout) params, bytes withdrawalValidatorSignature, bytes btcSpendAuthSignature) external returns (bool)"
19676
- ]);
19677
- const calldata = iface.encodeFunctionData("withdrawBTC", [
19678
- withdrawalParams,
19679
- signatureBytes,
19680
- btcSpendAuthBytes
19681
- ]);
19682
- const signer = this.getSignerOrThrow();
19683
- const fromAddress = await signer.getAddress();
19684
- const estimatedGas = await estimateContractCallGasWithMargin(
19685
- this.getProviderOrThrow(),
19686
- fromAddress,
19687
- contractAddress,
19688
- calldata,
19689
- MINT_UCD_ESTIMATE_MARGIN_BPS,
19690
- WITHDRAW_BTC_GAS_CEILING
19691
- );
19692
- const gasLimit = estimatedGas ?? WITHDRAW_BTC_GAS_CEILING;
19693
- await awaitSafeSubmissionWindow(
19694
- Number(withdrawalParams.quantumTimestamp)
20557
+ const position = await this.getPosition(positionId);
20558
+ const btcAmount = (Number(withdrawalAmount) / 1e8).toFixed(8);
20559
+ const message = formatBTCWithdrawalFailureMessage(
20560
+ btcAmount,
20561
+ position?.vaultAddress || "unknown",
20562
+ withdrawalAddress,
20563
+ positionId
19695
20564
  );
19696
- tx = await signer.sendTransaction({
19697
- to: contractAddress,
19698
- data: calldata,
19699
- value: "0x0",
19700
- gasLimit
19701
- });
19702
- if (this.config.debug) {
19703
- log.info("\u2705 withdrawBTC raw transaction fallback succeeded", {
19704
- txHash: tx.hash,
19705
- gasLimit: gasLimit.toString(),
19706
- estimated: estimatedGas !== null
19707
- });
19708
- }
19709
- } catch (rawError) {
19710
- const rawErrorMsg = rawError instanceof Error ? rawError.message : String(rawError);
19711
- if (this.config.debug) {
19712
- log.error("\u274C withdrawBTC transaction failed (raw fallback also failed)", {
19713
- error: rawErrorMsg,
19714
- withdrawalParams,
19715
- signatureBytesLength: signatureBytes.length
19716
- });
19717
- }
19718
- try {
19719
- const position = await this.getPosition(positionId);
19720
- const btcAmount = (Number(withdrawalAmount) / 1e8).toFixed(8);
19721
- const message = formatBTCWithdrawalFailureMessage(
19722
- btcAmount,
19723
- position?.vaultAddress || "unknown",
19724
- withdrawalAddress,
19725
- positionId
19726
- );
19727
- await this._relayNotification(message);
19728
- } catch {
19729
- }
19730
- return {
19731
- success: false,
19732
- error: `Transaction submission failed: ${rawErrorMsg}`
19733
- };
20565
+ await this._relayNotification(message);
20566
+ } catch {
19734
20567
  }
20568
+ return {
20569
+ success: false,
20570
+ error: `Transaction submission failed: ${txErrorMsg}`
20571
+ };
19735
20572
  }
19736
20573
  if (this.config.debug) {
19737
20574
  log.info("\u2705 Transaction submitted, waiting for confirmation:", {
@@ -19754,6 +20591,12 @@ Error data: ${errorData || "none"}`
19754
20591
  error: `Transaction confirmation failed: ${waitErrorMsg}`
19755
20592
  };
19756
20593
  }
20594
+ if (!receipt) {
20595
+ return {
20596
+ success: false,
20597
+ error: `Transaction confirmation returned no receipt. Hash: ${tx.hash}`
20598
+ };
20599
+ }
19757
20600
  if (this.config.debug) {
19758
20601
  log.info("\u2705 BTC withdrawal confirmed", {
19759
20602
  blockNumber: receipt.blockNumber,
@@ -19869,7 +20712,8 @@ Error data: ${errorData || "none"}`
19869
20712
  targetAmount: request.targetAmount,
19870
20713
  networkFee: request.networkFee,
19871
20714
  chainId,
19872
- signer
20715
+ signer,
20716
+ ...request.utxos ? { utxos: request.utxos } : {}
19873
20717
  });
19874
20718
  const headers = {
19875
20719
  "Content-Type": "application/json",
@@ -19900,7 +20744,10 @@ Error data: ${errorData || "none"}`
19900
20744
  userSignature: envelope.userSignature,
19901
20745
  borrowerAddress: envelope.borrowerAddress,
19902
20746
  chainId: envelope.chainId,
19903
- timestamp: envelope.timestamp
20747
+ timestamp: envelope.timestamp,
20748
+ // P6/#8: the exact input set hashed into the user signature.
20749
+ // The server must forward this set verbatim to the signer.
20750
+ utxos: envelope.utxos
19904
20751
  })
19905
20752
  }
19906
20753
  );
@@ -20240,10 +21087,13 @@ Error data: ${errorData || "none"}`
20240
21087
  }
20241
21088
  },
20242
21089
  rpcUrl,
20243
- ...sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
21090
+ // Hardhat-ONLY: never emitted on prod chains (server-ignored there; provider comes
21091
+ // from the on-chain BitcoinProviderRegistry). The stale-spend action reads
21092
+ // `devBitcoinProviderUrl` directly on Hardhat — this legacy alias is redundant but
21093
+ // kept behind the Hardhat gate so no prod request carries the caller-looking field.
21094
+ ...this.config.chain === "hardhat" && sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
20244
21095
  // Hardhat (chainId 1337/31337) has no on-chain BitcoinProviderRegistry —
20245
- // Lit Actions read `devBitcoinProviderUrl` from globalThis. Always pass;
20246
- // non-hardhat chains ignore it. (Single source of truth: bitcoinProviderUrl.)
21096
+ // Lit Actions read `devBitcoinProviderUrl` from globalThis. Non-hardhat chains ignore it.
20247
21097
  ...sdkBitcoinProviderUrl && { devBitcoinProviderUrl: sdkBitcoinProviderUrl },
20248
21098
  ...params.minConfirmations !== void 0 && {
20249
21099
  minConfirmations: params.minConfirmations
@@ -20566,6 +21416,15 @@ Error data: ${errorData || "none"}`
20566
21416
  async getLoanEvents(positionId, filter) {
20567
21417
  return this.loanQuery.getLoanEvents(positionId, filter);
20568
21418
  }
21419
+ /**
21420
+ * Get loan-event history for every position owned by a wallet.
21421
+ * Composes getUserPositions + getLoanEvents; returns one LoanEvents per
21422
+ * position (each carrying its own positionId). Bitcoin deposits are not on the
21423
+ * subgraph — merge those in from an Esplora source on the consumer side.
21424
+ */
21425
+ async getWalletLoanEvents(userAddress, filter) {
21426
+ return this.loanQuery.getWalletLoanEvents(userAddress, filter);
21427
+ }
20569
21428
  /**
20570
21429
  * Get protocol-wide events across all indexed entity types.
20571
21430
  * Returns a merged, sorted discriminated-union timeline.
@@ -21022,6 +21881,38 @@ Error data: ${errorData || "none"}`
21022
21881
  }));
21023
21882
  }
21024
21883
  }
21884
+ /**
21885
+ * List a vault address's PENDING (not-yet-confirmed) incoming BTC deposits.
21886
+ *
21887
+ * A deposit is "pending" until it reaches the protocol's minimum confirmation
21888
+ * depth (6) — the confirmed balance endpoints (`getVaultBalance` /
21889
+ * `getAddressBalance`) omit anything shallower, so this surfaces in-flight
21890
+ * deposits (0–5 confirmations). Server-delegated: lit-ops-server queries its
21891
+ * configured premium Esplora provider (`/api/lit/pending-deposits`). Requires
21892
+ * service mode.
21893
+ */
21894
+ async getPendingDeposits(vaultAddress) {
21895
+ this.ensureInitialized();
21896
+ if (!isServiceModeConfig(this.config)) {
21897
+ throw new Error("getPendingDeposits requires service mode \u2014 standalone mode is not supported");
21898
+ }
21899
+ const trimmedAddress = vaultAddress?.trim();
21900
+ if (!trimmedAddress) {
21901
+ throw new Error("vaultAddress is required");
21902
+ }
21903
+ const serviceEndpoint = this.config.serviceEndpoint;
21904
+ const url = `${serviceEndpoint}/api/lit/pending-deposits?vaultAddress=${encodeURIComponent(trimmedAddress)}`;
21905
+ const response = await fetch(url, {
21906
+ headers: await this.getAuthHeader()
21907
+ });
21908
+ if (!response.ok) {
21909
+ throw new Error(`Pending deposits request failed: ${response.status} ${response.statusText}`);
21910
+ }
21911
+ const json = await response.json();
21912
+ if (!json.success)
21913
+ throw new Error(json.error ?? "Pending deposits request failed");
21914
+ return json.data ?? [];
21915
+ }
21025
21916
  // ============================================================================
21026
21917
  // HIGH-LEVEL API - Mock Token Operations (Test Networks Only)
21027
21918
  // ============================================================================
@@ -21517,6 +22408,20 @@ Error data: ${errorData || "none"}`
21517
22408
  if (this.config.debug) {
21518
22409
  log.info(`\u{1F310} Calling liquidation server endpoint: ${endpoint}`);
21519
22410
  }
22411
+ const signer = this.getSignerOrThrow();
22412
+ const intentSigner = await signer.getAddress();
22413
+ const chainId = Number(
22414
+ (await this.getProviderOrThrow().getNetwork()).chainId
22415
+ );
22416
+ const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
22417
+ const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
22418
+ const intentTimestamp = Math.floor(Date.now() / 1e3);
22419
+ const intentActionHash = keccak2565(toUtf8Bytes5("liquidate-position"));
22420
+ const intentHash = solidityPackedKeccak2564(
22421
+ ["bytes32", "uint256", "uint256", "address", "bytes32"],
22422
+ [canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
22423
+ );
22424
+ const intentSignature = await signer.signMessage(getBytes4(intentHash));
21520
22425
  const response = await fetch(endpoint, {
21521
22426
  method: "POST",
21522
22427
  headers: { "Content-Type": "application/json" },
@@ -21532,6 +22437,11 @@ Error data: ${errorData || "none"}`
21532
22437
  UCDController: params.contractAddresses.ucdController,
21533
22438
  LiquidationManagerModule: params.contractAddresses.liquidationManager
21534
22439
  },
22440
+ liquidatorIntent: {
22441
+ timestamp: intentTimestamp,
22442
+ chainId,
22443
+ signature: intentSignature
22444
+ },
21535
22445
  rpcUrl: params.rpcUrl || this.config.ethRpcUrl,
21536
22446
  priceProviders: this.config.priceProviders,
21537
22447
  pkpTokenId: params.pkpData.tokenId,