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