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