@gvnrdao/dh-sdk 0.0.300 → 0.0.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +137 -30
- package/dist/deployments.mjs +137 -30
- package/dist/graphs/diamond-hands.d.ts +18 -0
- package/dist/index.js +1839 -929
- package/dist/index.mjs +1747 -837
- package/dist/interfaces/chunks/config.i.d.ts +22 -0
- package/dist/modules/diamond-hands-sdk.d.ts +41 -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/chunks/eip1559-broadcast.utils.d.ts +15 -24
- package/dist/utils/quantum-revert.utils.d.ts +9 -0
- package/dist/utils/quantum-submission.utils.d.ts +93 -0
- package/dist/utils/server-session-store.d.ts +56 -0
- package/dist/utils/server-session.d.ts +62 -9
- 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,85 @@ 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
|
|
431
|
+
},
|
|
432
|
+
CollateralManagerModule: {
|
|
433
|
+
previousImplementation: "0x1919860e528Cd2AbbD31a17858c986b8aCc8C185",
|
|
434
|
+
newImplementation: "0x55110B9dd6B2c9f812564e7a1ef42b3c76E1A3Bb",
|
|
435
|
+
upgradedAt: "2026-07-08T20:33:59Z",
|
|
436
|
+
upgradedBy: "AdminModule generic-op 0x740efe0cfe85044479cbd8ee3058aa244057d90703dd2b37044dd6787cbf3485 (3-of-5 ADMIN_ROLE Safes, 24h timelock; executed by 0xfb3B8cbeFB7D666A6Af34C53e189EdC31862a7be in tx 0x430c8e450f12d3d9359cde356ac5e2ef39661d7a89884b5674a596c1090f6cff)",
|
|
437
|
+
reason: "Validator gas fix (#420): BitcoinAddressValidator packed-table lookups; VERSION 1.3.0 -> 1.3.1"
|
|
438
|
+
},
|
|
439
|
+
BitcoinWithdrawalAddressRegistry: {
|
|
440
|
+
previousImplementation: "0x11731bFEC3068F2cc19D016381dfEA3B93013b1C",
|
|
441
|
+
newImplementation: "0x190c08300Ae650fb88ed8156445BBeb13bE9E8a3",
|
|
442
|
+
upgradedAt: "2026-07-08T20:33:59Z",
|
|
443
|
+
upgradedBy: "AdminModule generic-op 0x2603a486c0f34554b864cc1d909b325c7e105331da10ec0fd7e0b9373fa76207 (3-of-5 ADMIN_ROLE Safes, 24h timelock; executed by 0xfb3B8cbeFB7D666A6Af34C53e189EdC31862a7be in tx 0x430c8e450f12d3d9359cde356ac5e2ef39661d7a89884b5674a596c1090f6cff)",
|
|
444
|
+
reason: "Validator gas fix (#420): BitcoinAddressValidator packed-table lookups; VERSION 1.0.0 -> 1.0.1"
|
|
445
|
+
},
|
|
446
|
+
UCDController: {
|
|
447
|
+
newImplementation: "0x28B8cAB89D89B3F83581FD2290de2Ce84535bd0A",
|
|
448
|
+
upgradedBy: "Reconciled from chain \u2014 Upgraded event log unavailable; provenance omitted (never fabricated)",
|
|
449
|
+
reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)"
|
|
450
|
+
},
|
|
451
|
+
PositionManagerCoreModule: {
|
|
452
|
+
previousImplementation: "0x6C8b0FB93F8d0239254aefaC24d4264cA2F8AAeB",
|
|
453
|
+
newImplementation: "0x5067Ea1d40b2Aaf4ad693bD9b6e7463C1A29E849",
|
|
454
|
+
upgradedAt: "2026-07-06T08:02:35.000Z",
|
|
455
|
+
upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0x2002827d34A781883Ec4c83c36ECC140c3911F2B in tx 0xeb2bfe77d46394b6d0afd46beb0a80cd9beb91f6771ccb59ff9fba27d4af4f3d (block 25472167)",
|
|
456
|
+
reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
|
|
457
|
+
executeTx: "0xeb2bfe77d46394b6d0afd46beb0a80cd9beb91f6771ccb59ff9fba27d4af4f3d",
|
|
458
|
+
block: 25472167
|
|
459
|
+
},
|
|
460
|
+
LiquidationManagerModule: {
|
|
461
|
+
previousImplementation: "0xc2f4517eeD7c4c516939a64c7938a6ab432D99c4",
|
|
462
|
+
newImplementation: "0xe7444C1d9652dAb31BaAdBE4C7c7e7Df3515dA46",
|
|
463
|
+
upgradedAt: "2026-07-06T10:40:47.000Z",
|
|
464
|
+
upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0xc173b97af68cc9172ea0ddc91a2c90bf02eac1505e749d92aa044aa69891f48c (block 25472956)",
|
|
465
|
+
reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
|
|
466
|
+
executeTx: "0xc173b97af68cc9172ea0ddc91a2c90bf02eac1505e749d92aa044aa69891f48c",
|
|
467
|
+
block: 25472956
|
|
468
|
+
},
|
|
469
|
+
BTCSpendAuthorizer: {
|
|
470
|
+
previousImplementation: "0xb003ed89F695E8819a85513fE17260b805B3c478",
|
|
471
|
+
newImplementation: "0x40c9e8C46C65E0A57BB63da5368868A9E8Df81A5",
|
|
472
|
+
upgradedAt: "2026-07-06T10:40:59.000Z",
|
|
473
|
+
upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0xe1b1884a7e76c7fd569522545a04e49d5128d83d27b3892cda59f143b53b5281 (block 25472957)",
|
|
474
|
+
reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
|
|
475
|
+
executeTx: "0xe1b1884a7e76c7fd569522545a04e49d5128d83d27b3892cda59f143b53b5281",
|
|
476
|
+
block: 25472957
|
|
477
|
+
},
|
|
478
|
+
PositionManager: {
|
|
479
|
+
previousImplementation: "0x9f52BaBB40FbFC5863e45B1A4014cB433950c684",
|
|
480
|
+
newImplementation: "0x600Dc0D1C2c0F94882B6Bc09172f8650047Ad5B1",
|
|
481
|
+
upgradedAt: "2026-07-06T08:42:47.000Z",
|
|
482
|
+
upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0x2002827d34A781883Ec4c83c36ECC140c3911F2B in tx 0xa1fbe31cb71736514a868e681573dff22c9fab91d69cec157a24a30e29eadbe6 (block 25472368)",
|
|
483
|
+
reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
|
|
484
|
+
executeTx: "0xa1fbe31cb71736514a868e681573dff22c9fab91d69cec157a24a30e29eadbe6",
|
|
485
|
+
block: 25472368
|
|
486
|
+
},
|
|
487
|
+
LoanOperationsManagerModule: {
|
|
488
|
+
previousImplementation: "0x56671D671e492Dae625413d463D47605Cf54edf6",
|
|
489
|
+
newImplementation: "0x7d90aAB4466f59BA9aC9C7a9BF14dC53734336e7",
|
|
490
|
+
upgradedAt: "2026-07-06T10:41:23.000Z",
|
|
491
|
+
upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x327ae9f7a3917788d22dfe95611e1afcd18583f210b03a119d9f04eca50b9e7a (block 25472959)",
|
|
492
|
+
reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
|
|
493
|
+
executeTx: "0x327ae9f7a3917788d22dfe95611e1afcd18583f210b03a119d9f04eca50b9e7a",
|
|
494
|
+
block: 25472959
|
|
495
|
+
},
|
|
496
|
+
OperationAuthorizationRegistry: {
|
|
497
|
+
previousImplementation: "0x327662917Ac8EEda47851d74AF6A151348636fCe",
|
|
498
|
+
newImplementation: "0x493118746840EDB2D396E71632D2E774CA14070e",
|
|
499
|
+
upgradedAt: "2026-07-06T10:41:47.000Z",
|
|
500
|
+
upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0x9820e259700865a281b8376e248044048ba2fed1d17c811ac66ac54d36711a5b (block 25472961)",
|
|
501
|
+
reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
|
|
502
|
+
executeTx: "0x9820e259700865a281b8376e248044048ba2fed1d17c811ac66ac54d36711a5b",
|
|
503
|
+
block: 25472961
|
|
425
504
|
}
|
|
426
505
|
},
|
|
427
506
|
latestEnv: {
|
|
@@ -434,9 +513,37 @@ var init_deployment_addresses = __esm({
|
|
|
434
513
|
BITCOIN_PROVIDER_REGISTRY: "0x4ab3Fb96e02893Ed6FAa61bad1918390c8DbbFFC",
|
|
435
514
|
BITCOIN_PROVIDER_REGISTRY_IMPL: "0x7Bc9473E45fb3EA594e8b13992F57D0444902ddB",
|
|
436
515
|
BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY: "0xf667f77FDba7458345f227eB6Bfe4148243E7270",
|
|
437
|
-
BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "
|
|
516
|
+
BITCOIN_WITHDRAWAL_ADDRESS_REGISTRY_IMPL: "0x190c08300Ae650fb88ed8156445BBeb13bE9E8a3",
|
|
438
517
|
SIMPLE_PSM_V2: "0xfE0C14B4E614f40Bf58845b4B04CeD17fd116532",
|
|
439
|
-
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"
|
|
440
547
|
}
|
|
441
548
|
};
|
|
442
549
|
MAINNET_CONTRACTS = {
|
|
@@ -467,44 +574,44 @@ var init_deployment_addresses = __esm({
|
|
|
467
574
|
LOCALHOST_DEPLOYMENT = {
|
|
468
575
|
network: "localhost",
|
|
469
576
|
chainId: 31337,
|
|
470
|
-
timestamp: "2026-
|
|
577
|
+
timestamp: "2026-06-11T12:30:59.589Z",
|
|
471
578
|
deployer: "",
|
|
472
579
|
contracts: {
|
|
473
|
-
MessageHashBuilder: "
|
|
474
|
-
UpgradeValidator: "
|
|
580
|
+
MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
|
|
581
|
+
UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
|
|
475
582
|
UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
|
476
583
|
UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
|
|
477
|
-
PositionManagerCoreModule: "
|
|
478
|
-
TermManagerModule: "
|
|
479
|
-
LoanOperationsManagerModule: "
|
|
584
|
+
PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
|
|
585
|
+
TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
|
|
586
|
+
LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
|
|
480
587
|
BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
|
|
481
588
|
CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
|
|
482
589
|
LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
|
|
483
|
-
CircuitBreakerModule: "
|
|
590
|
+
CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
|
|
484
591
|
AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
|
|
485
|
-
PositionManagerViews: "
|
|
486
|
-
PositionManager: "
|
|
487
|
-
OperationAuthorizationRegistry: "
|
|
488
|
-
PKPValidation: "
|
|
592
|
+
PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
|
|
593
|
+
PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
|
|
594
|
+
OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
|
|
595
|
+
PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
|
|
489
596
|
}
|
|
490
597
|
};
|
|
491
598
|
LOCALHOST_CONTRACTS = {
|
|
492
|
-
MessageHashBuilder: "
|
|
493
|
-
UpgradeValidator: "
|
|
599
|
+
MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
|
|
600
|
+
UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
|
|
494
601
|
UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
|
495
602
|
UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
|
|
496
|
-
PositionManagerCoreModule: "
|
|
497
|
-
TermManagerModule: "
|
|
498
|
-
LoanOperationsManagerModule: "
|
|
603
|
+
PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
|
|
604
|
+
TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
|
|
605
|
+
LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
|
|
499
606
|
BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
|
|
500
607
|
CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
|
|
501
608
|
LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
|
|
502
|
-
CircuitBreakerModule: "
|
|
609
|
+
CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
|
|
503
610
|
AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
|
|
504
|
-
PositionManagerViews: "
|
|
505
|
-
PositionManager: "
|
|
506
|
-
OperationAuthorizationRegistry: "
|
|
507
|
-
PKPValidation: "
|
|
611
|
+
PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
|
|
612
|
+
PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
|
|
613
|
+
OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
|
|
614
|
+
PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
|
|
508
615
|
};
|
|
509
616
|
ALL_DEPLOYMENTS = {
|
|
510
617
|
sepolia: SEPOLIA_DEPLOYMENT,
|
|
@@ -1196,30 +1303,30 @@ var require_pkg_src = __commonJS({
|
|
|
1196
1303
|
}
|
|
1197
1304
|
},
|
|
1198
1305
|
btcTransactionSigner: {
|
|
1199
|
-
cid: "
|
|
1306
|
+
cid: "QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk",
|
|
1200
1307
|
authorizedCidHex: cidToHex(
|
|
1201
|
-
"
|
|
1308
|
+
"QmSJkQAw5SJ39EGEwzWZJ39ApNN4834VgKS5rhm25YCgfk"
|
|
1202
1309
|
),
|
|
1203
1310
|
name: "Btc Transaction Signer",
|
|
1204
1311
|
description: "Production Btc Transaction Signer",
|
|
1205
|
-
version: "1.
|
|
1312
|
+
version: "1.1.1",
|
|
1206
1313
|
deployed: true,
|
|
1207
|
-
deployedAt:
|
|
1208
|
-
size:
|
|
1209
|
-
hash: "
|
|
1314
|
+
deployedAt: 1784316496778,
|
|
1315
|
+
size: 63679,
|
|
1316
|
+
hash: "b12aa725797af5ef07f8e6945f05741f583d6e479a4670b1121e97c58fe63191"
|
|
1210
1317
|
},
|
|
1211
1318
|
ucdMintValidator: {
|
|
1212
|
-
cid: "
|
|
1319
|
+
cid: "QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW",
|
|
1213
1320
|
authorizedCidHex: cidToHex(
|
|
1214
|
-
"
|
|
1321
|
+
"QmYVRMfiSC641JPbE819szSDw3YrG6uSrZc27CZkyYTGtW"
|
|
1215
1322
|
),
|
|
1216
1323
|
name: "Ucd Mint Validator",
|
|
1217
1324
|
description: "Production Ucd Mint Validator",
|
|
1218
|
-
version: "1.
|
|
1325
|
+
version: "1.2.1",
|
|
1219
1326
|
deployed: true,
|
|
1220
|
-
deployedAt:
|
|
1221
|
-
size:
|
|
1222
|
-
hash: "
|
|
1327
|
+
deployedAt: 1784316505431,
|
|
1328
|
+
size: 80381,
|
|
1329
|
+
hash: "132a0a8e6e9a1b85d02e00c0e57eaa371d825717255a28c9e644e4b521760026",
|
|
1223
1330
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1224
1331
|
pkp: {
|
|
1225
1332
|
publicKey: "0x041ab17cd91fc5c0b761eea6092d032807561b621b82c488826776e04a9158d61ba64d809a4729f0501289d980732cb06d8dfd06999dd9c8efd2f495dad78b31bb",
|
|
@@ -1227,17 +1334,17 @@ var require_pkg_src = __commonJS({
|
|
|
1227
1334
|
}
|
|
1228
1335
|
},
|
|
1229
1336
|
processPaymentValidator: {
|
|
1230
|
-
cid: "
|
|
1337
|
+
cid: "QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ",
|
|
1231
1338
|
authorizedCidHex: cidToHex(
|
|
1232
|
-
"
|
|
1339
|
+
"QmadmdFDH6sS7YLusUuuLkkt7n51iUj1bJ5KKTLhmro2iQ"
|
|
1233
1340
|
),
|
|
1234
1341
|
name: "Process Payment Validator",
|
|
1235
1342
|
description: "Production Process Payment Validator",
|
|
1236
|
-
version: "1.
|
|
1343
|
+
version: "1.2.1",
|
|
1237
1344
|
deployed: true,
|
|
1238
|
-
deployedAt:
|
|
1239
|
-
size:
|
|
1240
|
-
hash: "
|
|
1345
|
+
deployedAt: 1784316520125,
|
|
1346
|
+
size: 76534,
|
|
1347
|
+
hash: "dbdf43d3f1636e0ee883d349ca28f24c5c38ed7e062b76ca75498aaf5fe594ea",
|
|
1241
1348
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1242
1349
|
pkp: {
|
|
1243
1350
|
publicKey: "0x04cfccb9b6c7addc79469c0e9fafebef4c70801302c62cdb26b610723a9b8527d23f98a40158c7340d83fd0e62d02b5ec74c12f7a86d98e8c6973b4811110f19ad",
|
|
@@ -1245,17 +1352,17 @@ var require_pkg_src = __commonJS({
|
|
|
1245
1352
|
}
|
|
1246
1353
|
},
|
|
1247
1354
|
extendPositionValidator: {
|
|
1248
|
-
cid: "
|
|
1355
|
+
cid: "QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4",
|
|
1249
1356
|
authorizedCidHex: cidToHex(
|
|
1250
|
-
"
|
|
1357
|
+
"QmespREceysQ7WPqGKg1VpHB3hf8zsreMY8HSjiQ2xoAN4"
|
|
1251
1358
|
),
|
|
1252
1359
|
name: "Extend Position Validator",
|
|
1253
1360
|
description: "Production Extend Position Validator",
|
|
1254
|
-
version: "1.
|
|
1361
|
+
version: "1.2.1",
|
|
1255
1362
|
deployed: true,
|
|
1256
|
-
deployedAt:
|
|
1257
|
-
size:
|
|
1258
|
-
hash: "
|
|
1363
|
+
deployedAt: 1784316534278,
|
|
1364
|
+
size: 74352,
|
|
1365
|
+
hash: "c17bfc9abef16f2372316a14c14281e2acc98afdd1662d095360f1d1c5383f1d",
|
|
1259
1366
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1260
1367
|
pkp: {
|
|
1261
1368
|
publicKey: "0x04e52338f6a0c3362800f1a94c4c8f08be5030dde0fa217388c53d1c1a4790f4443c9f2886b2d2d8f4b75a9a156e9b028011b1a1cf7648da0e556aa980ec692836",
|
|
@@ -1263,17 +1370,17 @@ var require_pkg_src = __commonJS({
|
|
|
1263
1370
|
}
|
|
1264
1371
|
},
|
|
1265
1372
|
btcWithdrawal: {
|
|
1266
|
-
cid: "
|
|
1373
|
+
cid: "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2",
|
|
1267
1374
|
authorizedCidHex: cidToHex(
|
|
1268
|
-
"
|
|
1375
|
+
"QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2"
|
|
1269
1376
|
),
|
|
1270
1377
|
name: "Btc Withdrawal",
|
|
1271
1378
|
description: "Production Btc Withdrawal",
|
|
1272
|
-
version: "1.
|
|
1379
|
+
version: "1.2.1",
|
|
1273
1380
|
deployed: true,
|
|
1274
|
-
deployedAt:
|
|
1275
|
-
size:
|
|
1276
|
-
hash: "
|
|
1381
|
+
deployedAt: 1784316549409,
|
|
1382
|
+
size: 90692,
|
|
1383
|
+
hash: "ca7276763142fe1fc812b70f6319973000faf338a91c531f898f8f6406035c64",
|
|
1277
1384
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1278
1385
|
pkp: {
|
|
1279
1386
|
publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
|
|
@@ -1281,17 +1388,17 @@ var require_pkg_src = __commonJS({
|
|
|
1281
1388
|
}
|
|
1282
1389
|
},
|
|
1283
1390
|
liquidationValidator: {
|
|
1284
|
-
cid: "
|
|
1391
|
+
cid: "QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx",
|
|
1285
1392
|
authorizedCidHex: cidToHex(
|
|
1286
|
-
"
|
|
1393
|
+
"QmZJnGq2HEYW1XYZPSodyRCrbDhTcLDT8x4z76EiPg8Xcx"
|
|
1287
1394
|
),
|
|
1288
1395
|
name: "Liquidation Validator",
|
|
1289
1396
|
description: "Production Liquidation Validator",
|
|
1290
|
-
version: "1.
|
|
1397
|
+
version: "1.2.1",
|
|
1291
1398
|
deployed: true,
|
|
1292
|
-
deployedAt:
|
|
1293
|
-
size:
|
|
1294
|
-
hash: "
|
|
1399
|
+
deployedAt: 1784316560010,
|
|
1400
|
+
size: 66523,
|
|
1401
|
+
hash: "f74d0163c47d7db04d1547f72c3a9bc1e5b499669089cc6464479a361462ce81",
|
|
1295
1402
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1296
1403
|
pkp: {
|
|
1297
1404
|
publicKey: "0x041a241de58976c13e3d7ca5a18e494f7330151e3818706135d50176e78ffb673e58b11f0b67a87c73fade6c6b27d6e7a7377ea9f4c10bf31880f1d725f18cc6f8",
|
|
@@ -1317,17 +1424,17 @@ var require_pkg_src = __commonJS({
|
|
|
1317
1424
|
}
|
|
1318
1425
|
},
|
|
1319
1426
|
loanVaultBtcBalance: {
|
|
1320
|
-
cid: "
|
|
1427
|
+
cid: "QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu",
|
|
1321
1428
|
authorizedCidHex: cidToHex(
|
|
1322
|
-
"
|
|
1429
|
+
"QmVKSkgY8e5X6v6yRD4H2wpU2oX2kbeKYh4fjPepg71mqu"
|
|
1323
1430
|
),
|
|
1324
1431
|
name: "Loan Vault Btc Balance",
|
|
1325
1432
|
description: "Production Loan Vault Btc Balance",
|
|
1326
|
-
version: "1.
|
|
1433
|
+
version: "1.2.1",
|
|
1327
1434
|
deployed: true,
|
|
1328
|
-
deployedAt:
|
|
1329
|
-
size:
|
|
1330
|
-
hash: "
|
|
1435
|
+
deployedAt: 1784316574669,
|
|
1436
|
+
size: 56477,
|
|
1437
|
+
hash: "917535f2911055b2ade2204fd868dd8b7294c7ade23e82aea488aeb0872e35d7",
|
|
1331
1438
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1332
1439
|
pkp: {
|
|
1333
1440
|
publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
|
|
@@ -1341,17 +1448,17 @@ var require_pkg_src = __commonJS({
|
|
|
1341
1448
|
// Requires BitcoinProviderRegistry.addAllowedCid(chainId, keccak256(utf8(cid)))
|
|
1342
1449
|
// on each target chain (the action decrypts registry slots). No OAR entry.
|
|
1343
1450
|
btcAddressBalance: {
|
|
1344
|
-
cid: "
|
|
1451
|
+
cid: "QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA",
|
|
1345
1452
|
authorizedCidHex: cidToHex(
|
|
1346
|
-
"
|
|
1453
|
+
"QmcYFLDbhQArk5gQK44Xj8CnYEAbnaNsei92sUHpRuFmZA"
|
|
1347
1454
|
),
|
|
1348
1455
|
name: "Btc Address Balance",
|
|
1349
1456
|
description: "Address-only signed total BTC balance via registry consensus (no positionId, no price)",
|
|
1350
|
-
version: "0.1
|
|
1457
|
+
version: "0.2.1",
|
|
1351
1458
|
deployed: true,
|
|
1352
|
-
deployedAt:
|
|
1353
|
-
size:
|
|
1354
|
-
hash: "
|
|
1459
|
+
deployedAt: 1784316590403,
|
|
1460
|
+
size: 26986,
|
|
1461
|
+
hash: "49fb14265f3208916f12806df04492900bd08fb1b14d9651ee6278cd9ae4a428",
|
|
1355
1462
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1356
1463
|
pkp: {
|
|
1357
1464
|
publicKey: "0x0498cbb31facfa26e3ec83ccdabfe4fe341ca0282e0c3cb81da7113723b9726e1e49efa92165cc3faa66ced4454cea56dbab12267852d6525a55d7c4f128b912cd",
|
|
@@ -1385,17 +1492,17 @@ var require_pkg_src = __commonJS({
|
|
|
1385
1492
|
hash: "03ee5be239c91c735d2c95db89a0d1b77e602cfc18af8eb64b3281fddcd90a72"
|
|
1386
1493
|
},
|
|
1387
1494
|
staleSpendRecoveryValidator: {
|
|
1388
|
-
cid: "
|
|
1495
|
+
cid: "QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK",
|
|
1389
1496
|
authorizedCidHex: cidToHex(
|
|
1390
|
-
"
|
|
1497
|
+
"QmcLrhiM1JwVi5w3QFxBYi1V4DaFT3Kk9MhPywyMnTsEwK"
|
|
1391
1498
|
),
|
|
1392
1499
|
name: "Stale Spend Recovery Validator",
|
|
1393
1500
|
description: "Production Stale Spend Recovery Validator",
|
|
1394
|
-
version: "1.
|
|
1501
|
+
version: "1.1.2",
|
|
1395
1502
|
deployed: true,
|
|
1396
|
-
deployedAt:
|
|
1397
|
-
size:
|
|
1398
|
-
hash: "
|
|
1503
|
+
deployedAt: 1784316604502,
|
|
1504
|
+
size: 36785,
|
|
1505
|
+
hash: "28bfe44970541fefe3bae8bd54f25e8775d7cc61b5832a40ba1c18587b6b6358",
|
|
1399
1506
|
validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
|
|
1400
1507
|
pkp: {
|
|
1401
1508
|
publicKey: "0x046da0ad6da7dd4a0063cbeac6f2bdb6a9889c2178b23470833356a8ee5630f7f0eee0db348d9d9aaaadd23eccf0063c8f68ee5e446abe30b882bf1de4c932bc21",
|
|
@@ -2146,7 +2253,7 @@ ${errorReport}`);
|
|
|
2146
2253
|
}
|
|
2147
2254
|
init_debug_logger();
|
|
2148
2255
|
init_session_signature_cache();
|
|
2149
|
-
var
|
|
2256
|
+
var import_ethers16 = __require("ethers");
|
|
2150
2257
|
var EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS = 11e3;
|
|
2151
2258
|
var GRACE_PERIOD_DAYS = 30;
|
|
2152
2259
|
var SATOSHIS_PER_BITCOIN = 100000000n;
|
|
@@ -2183,6 +2290,20 @@ ${errorReport}`);
|
|
|
2183
2290
|
/* REGTEST */
|
|
2184
2291
|
// Local development
|
|
2185
2292
|
};
|
|
2293
|
+
var CHAIN_TO_CHAIN_ID = {
|
|
2294
|
+
[
|
|
2295
|
+
"ethereum"
|
|
2296
|
+
/* ETHEREUM */
|
|
2297
|
+
]: 1,
|
|
2298
|
+
[
|
|
2299
|
+
"sepolia"
|
|
2300
|
+
/* SEPOLIA */
|
|
2301
|
+
]: 11155111,
|
|
2302
|
+
[
|
|
2303
|
+
"hardhat"
|
|
2304
|
+
/* HARDHAT */
|
|
2305
|
+
]: 1337
|
|
2306
|
+
};
|
|
2186
2307
|
function validateChain(chain) {
|
|
2187
2308
|
const normalized = chain.toLowerCase().trim();
|
|
2188
2309
|
if (normalized === "sepolia") {
|
|
@@ -2479,6 +2600,27 @@ ${errorReport}`);
|
|
|
2479
2600
|
function createProtocolParametersModule(config) {
|
|
2480
2601
|
return new ProtocolParametersModule(config);
|
|
2481
2602
|
}
|
|
2603
|
+
function parseVaultAddress(rawVaultAddress, chain) {
|
|
2604
|
+
try {
|
|
2605
|
+
const parsed = JSON.parse(rawVaultAddress);
|
|
2606
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2607
|
+
const chainLower = chain.toLowerCase();
|
|
2608
|
+
if (chainLower === "sepolia") {
|
|
2609
|
+
return parsed.regtest || parsed.testnet || parsed.mainnet;
|
|
2610
|
+
}
|
|
2611
|
+
const bitcoinNetwork = getBitcoinNetworkForChain(chain);
|
|
2612
|
+
if (bitcoinNetwork === "regtest") {
|
|
2613
|
+
return parsed.regtest || parsed.testnet || parsed.mainnet;
|
|
2614
|
+
}
|
|
2615
|
+
if (bitcoinNetwork === "testnet") {
|
|
2616
|
+
return parsed.testnet || parsed.regtest || parsed.mainnet;
|
|
2617
|
+
}
|
|
2618
|
+
return parsed.mainnet;
|
|
2619
|
+
}
|
|
2620
|
+
} catch (e) {
|
|
2621
|
+
}
|
|
2622
|
+
return rawVaultAddress;
|
|
2623
|
+
}
|
|
2482
2624
|
var VaultSnapshotModule = class {
|
|
2483
2625
|
constructor(config) {
|
|
2484
2626
|
this.config = config;
|
|
@@ -2586,112 +2728,11 @@ ${errorReport}`);
|
|
|
2586
2728
|
timestamp: Date.now()
|
|
2587
2729
|
};
|
|
2588
2730
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
* For authorization/security operations, ALWAYS use getVaultSnapshot().
|
|
2595
|
-
*
|
|
2596
|
-
* @param positionId - Position to snapshot
|
|
2597
|
-
* @returns Complete VaultSnapshot with all metrics (using fast balance method)
|
|
2598
|
-
*/
|
|
2599
|
-
async getVaultSnapshotFast(positionId) {
|
|
2600
|
-
const positionState = await this.queryPositionState(positionId);
|
|
2601
|
-
console.log(
|
|
2602
|
-
`[Vault Snapshot] Raw vault address from contract: "${positionState.vaultAddress}"`
|
|
2603
|
-
);
|
|
2604
|
-
const useStubbedBtcData = this.config.debugOverrides?.useStubbedBtcData === true;
|
|
2605
|
-
const [balanceResult, btcPriceUsd] = useStubbedBtcData ? this.buildStubbedBtcSnapshotInputs(positionState) : await Promise.all([
|
|
2606
|
-
this.config.vaultBalance.calculateTrustedBalance(
|
|
2607
|
-
positionId,
|
|
2608
|
-
positionState.vaultAddress,
|
|
2609
|
-
true
|
|
2610
|
-
// Always skip UTXO validation for fast path
|
|
2611
|
-
),
|
|
2612
|
-
this.config.priceOracle.getBTCPrice()
|
|
2613
|
-
]);
|
|
2614
|
-
const protocolParams = createProtocolParametersModule({
|
|
2615
|
-
termManagerAddress: this.config.termManagerAddress,
|
|
2616
|
-
loanOpsManagerAddress: this.config.loanOpsManagerAddress,
|
|
2617
|
-
liquidationManagerAddress: this.config.liquidationManagerAddress,
|
|
2618
|
-
chain: this.config.chain,
|
|
2619
|
-
chainId: this.config.chainId || 1,
|
|
2620
|
-
// Default to mainnet if not provided
|
|
2621
|
-
rpcUrl: this.config.rpcUrl
|
|
2622
|
-
});
|
|
2623
|
-
const termForFees = this.config.termOverride ?? positionState.selectedTerm;
|
|
2624
|
-
const [liquidationThresholdBps, maxEscalatedThresholdBps, termFees] = await Promise.all([
|
|
2625
|
-
protocolParams.getLiquidationThreshold(),
|
|
2626
|
-
protocolParams.getMaxEscalatedThreshold(),
|
|
2627
|
-
protocolParams.getTermFees(termForFees)
|
|
2628
|
-
]);
|
|
2629
|
-
const termStatus = calculateTermStatus(
|
|
2630
|
-
positionState.termStartTimestamp,
|
|
2631
|
-
positionState.selectedTerm
|
|
2632
|
-
);
|
|
2633
|
-
const currentLiquidationThreshold = calculateLiquidationThreshold(
|
|
2634
|
-
termStatus.isExpired,
|
|
2635
|
-
termStatus.daysIntoGracePeriod,
|
|
2636
|
-
liquidationThresholdBps,
|
|
2637
|
-
maxEscalatedThresholdBps
|
|
2638
|
-
);
|
|
2639
|
-
const collateralMetrics = calculateCollateralMetrics(
|
|
2640
|
-
balanceResult.availableBalance,
|
|
2641
|
-
btcPriceUsd,
|
|
2642
|
-
positionState.ucdDebt
|
|
2643
|
-
);
|
|
2644
|
-
const isLiquidatable = collateralMetrics.collateralRatioBps < currentLiquidationThreshold;
|
|
2645
|
-
const marginToLiquidationBps = collateralMetrics.collateralRatioBps - currentLiquidationThreshold;
|
|
2646
|
-
return {
|
|
2647
|
-
// Raw state
|
|
2648
|
-
positionId: positionState.positionId,
|
|
2649
|
-
pkpId: positionState.pkpId,
|
|
2650
|
-
borrower: positionState.borrower,
|
|
2651
|
-
vaultAddress: positionState.vaultAddress,
|
|
2652
|
-
ucdDebt: positionState.ucdDebt,
|
|
2653
|
-
termStartTimestamp: positionState.termStartTimestamp,
|
|
2654
|
-
selectedTerm: positionState.selectedTerm,
|
|
2655
|
-
status: positionState.status,
|
|
2656
|
-
// Loan status enum value (0-7)
|
|
2657
|
-
expiryAt: positionState.expiryAt,
|
|
2658
|
-
// Loan expiry timestamp
|
|
2659
|
-
previousExpiryAt: positionState.previousExpiryAt,
|
|
2660
|
-
// expiryAt before last renewal; 0 if never renewed
|
|
2661
|
-
totalTerm: positionState.totalTerm,
|
|
2662
|
-
// total accumulated loan months; 0 before first renewal
|
|
2663
|
-
lastRenewedAt: positionState.lastRenewedAt,
|
|
2664
|
-
// timestamp of most recent renewal; createdAt at origination; 0 for pre-H-5 positions
|
|
2665
|
-
// Bitcoin state
|
|
2666
|
-
totalBTCSats: balanceResult.totalBalance,
|
|
2667
|
-
totalUTXOs: balanceResult.totalUTXOs,
|
|
2668
|
-
authorizedSpendsSats: balanceResult.authorizedBalance,
|
|
2669
|
-
authorizedSpendsHash: balanceResult.authorizedSpendsHash,
|
|
2670
|
-
availableBTCSats: balanceResult.availableBalance,
|
|
2671
|
-
availableUTXOs: balanceResult.availableUTXOs,
|
|
2672
|
-
// Price data
|
|
2673
|
-
btcPriceUsd,
|
|
2674
|
-
// Calculated metrics
|
|
2675
|
-
collateralValueUsd: collateralMetrics.collateralValueUsd,
|
|
2676
|
-
collateralRatioBps: collateralMetrics.collateralRatioBps,
|
|
2677
|
-
// Term status
|
|
2678
|
-
termDurationDays: termStatus.termDurationDays,
|
|
2679
|
-
termLengthDays: termStatus.termLengthDays,
|
|
2680
|
-
isExpired: termStatus.isExpired,
|
|
2681
|
-
daysUntilExpiry: termStatus.daysUntilExpiry,
|
|
2682
|
-
daysIntoGracePeriod: termStatus.daysIntoGracePeriod,
|
|
2683
|
-
// Liquidation risk
|
|
2684
|
-
currentLiquidationThreshold,
|
|
2685
|
-
isLiquidatable,
|
|
2686
|
-
marginToLiquidationBps,
|
|
2687
|
-
// Protocol parameters (from smart contracts)
|
|
2688
|
-
liquidationThresholdBps,
|
|
2689
|
-
originationFeeBps: termFees.originationFeeBps,
|
|
2690
|
-
extensionFeeBps: termFees.extensionFeeBps,
|
|
2691
|
-
// Metadata
|
|
2692
|
-
timestamp: Date.now()
|
|
2693
|
-
};
|
|
2694
|
-
}
|
|
2731
|
+
// NOTE (remediation P1/#5): `getVaultSnapshotFast` — a snapshot built on the
|
|
2732
|
+
// skip-UTXO-validation balance path — was deleted. It had zero callers, and
|
|
2733
|
+
// a snapshot whose balance skipped UTXO validation + consensus must never
|
|
2734
|
+
// exist within reach of a signing flow. Every snapshot now runs the full
|
|
2735
|
+
// validation path above.
|
|
2695
2736
|
buildStubbedBtcSnapshotInputs(positionState) {
|
|
2696
2737
|
const stubbedAvailableBtcSats = BigInt(
|
|
2697
2738
|
this.config.debugOverrides?.stubbedAvailableBtcSats ?? "10000000"
|
|
@@ -2820,14 +2861,20 @@ ${errorReport}`);
|
|
|
2820
2861
|
expiryAt,
|
|
2821
2862
|
selectedTermMonths
|
|
2822
2863
|
);
|
|
2864
|
+
const parsedVaultAddress = parseVaultAddress(
|
|
2865
|
+
position.vaultAddress,
|
|
2866
|
+
validateChain(this.config.chain)
|
|
2867
|
+
);
|
|
2868
|
+
if (typeof parsedVaultAddress !== "string" || parsedVaultAddress.length === 0) {
|
|
2869
|
+
throw new Error(
|
|
2870
|
+
`Position ${positionIdBytes32} vaultAddress has no entry for chain "${this.config.chain}"`
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2823
2873
|
return {
|
|
2824
2874
|
positionId: position.positionId,
|
|
2825
2875
|
pkpId: position.pkpId,
|
|
2826
2876
|
borrower: position.borrower,
|
|
2827
|
-
vaultAddress:
|
|
2828
|
-
position.vaultAddress,
|
|
2829
|
-
validateChain(this.config.chain)
|
|
2830
|
-
),
|
|
2877
|
+
vaultAddress: parsedVaultAddress,
|
|
2831
2878
|
ucdDebt: BigInt(position.ucdDebt.toString()),
|
|
2832
2879
|
termStartTimestamp,
|
|
2833
2880
|
selectedTerm: selectedTermMonths,
|
|
@@ -2838,36 +2885,8 @@ ${errorReport}`);
|
|
|
2838
2885
|
lastRenewedAt
|
|
2839
2886
|
};
|
|
2840
2887
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
*
|
|
2844
|
-
* Supports two formats:
|
|
2845
|
-
* 1. Single string: "mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"
|
|
2846
|
-
* 2. JSON object: {"mainnet":"bc1q...","testnet":"mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"}
|
|
2847
|
-
*
|
|
2848
|
-
* @param rawVaultAddress Raw vault address from contract
|
|
2849
|
-
* @param chain EVM chain (sepolia, ethereum)
|
|
2850
|
-
* @returns Parsed Bitcoin address for the appropriate network
|
|
2851
|
-
*/
|
|
2852
|
-
parseVaultAddress(rawVaultAddress, chain) {
|
|
2853
|
-
try {
|
|
2854
|
-
const parsed = JSON.parse(rawVaultAddress);
|
|
2855
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
2856
|
-
const chainLower = chain.toLowerCase();
|
|
2857
|
-
if (chainLower === "sepolia") {
|
|
2858
|
-
return parsed.regtest || parsed.testnet || parsed.mainnet;
|
|
2859
|
-
}
|
|
2860
|
-
const bitcoinNetwork = getBitcoinNetworkForChain(chain);
|
|
2861
|
-
if (bitcoinNetwork === "testnet") {
|
|
2862
|
-
return parsed.testnet || parsed.regtest || parsed.mainnet;
|
|
2863
|
-
} else {
|
|
2864
|
-
return parsed.mainnet;
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
} catch (e) {
|
|
2868
|
-
}
|
|
2869
|
-
return rawVaultAddress;
|
|
2870
|
-
}
|
|
2888
|
+
// parseVaultAddress moved to `modules/vault-address.ts` (shared with the
|
|
2889
|
+
// phase-2 signer for the P6/#7b vault-identity binding).
|
|
2871
2890
|
/**
|
|
2872
2891
|
* Quick check if position is liquidatable
|
|
2873
2892
|
*
|
|
@@ -3205,6 +3224,13 @@ ${auth.clientId}`;
|
|
|
3205
3224
|
return {};
|
|
3206
3225
|
}
|
|
3207
3226
|
}
|
|
3227
|
+
function assertSaneSatoshis(value, context) {
|
|
3228
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_BTC_AMOUNT) {
|
|
3229
|
+
throw new Error(
|
|
3230
|
+
`${context}: satoshi value ${value} is not a sane amount (must be an integer in [0, ${MAX_BTC_AMOUNT}])`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3208
3234
|
var BitcoinDataProvider = class {
|
|
3209
3235
|
constructor(config) {
|
|
3210
3236
|
this.config = config;
|
|
@@ -3351,13 +3377,16 @@ ${auth.clientId}`;
|
|
|
3351
3377
|
address
|
|
3352
3378
|
);
|
|
3353
3379
|
const utxos = unspentOutputs.map(
|
|
3354
|
-
(utxo) =>
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3380
|
+
(utxo) => {
|
|
3381
|
+
const satNumber = Math.round(utxo.amount * 1e8);
|
|
3382
|
+
assertSaneSatoshis(satNumber, `fetchUTXOsFromRPC ${utxo.txid}:${utxo.vout}`);
|
|
3383
|
+
return {
|
|
3384
|
+
txid: utxo.txid,
|
|
3385
|
+
vout: utxo.vout,
|
|
3386
|
+
satoshis: BigInt(satNumber),
|
|
3387
|
+
confirmations: utxo.confirmations
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3361
3390
|
);
|
|
3362
3391
|
return utxos;
|
|
3363
3392
|
}
|
|
@@ -3399,6 +3428,7 @@ ${auth.clientId}`;
|
|
|
3399
3428
|
"Invalid UTXO row: missing txid, vout/n, or value/satoshis"
|
|
3400
3429
|
);
|
|
3401
3430
|
}
|
|
3431
|
+
assertSaneSatoshis(satRaw, `parseBlockstreamUTXOs ${txid}:${vout}`);
|
|
3402
3432
|
utxos.push({
|
|
3403
3433
|
txid,
|
|
3404
3434
|
vout,
|
|
@@ -3577,7 +3607,10 @@ ${auth.clientId}`;
|
|
|
3577
3607
|
`Invalid response: missing required fields. funded_txo_sum: ${fundedTxoSum}, spent_txo_sum: ${spentTxoSum}`
|
|
3578
3608
|
);
|
|
3579
3609
|
}
|
|
3610
|
+
assertSaneSatoshis(fundedTxoSum, `getBalance ${cleanAddress} funded_txo_sum`);
|
|
3611
|
+
assertSaneSatoshis(spentTxoSum, `getBalance ${cleanAddress} spent_txo_sum`);
|
|
3580
3612
|
const confirmedBalance = fundedTxoSum - spentTxoSum;
|
|
3613
|
+
assertSaneSatoshis(confirmedBalance, `getBalance ${cleanAddress} confirmed balance`);
|
|
3581
3614
|
const balance = BigInt(confirmedBalance);
|
|
3582
3615
|
this.logger.log("3", `Balance: ${balance.toString()} sats`);
|
|
3583
3616
|
this.logger.log(
|
|
@@ -4056,20 +4089,590 @@ ${auth.clientId}`;
|
|
|
4056
4089
|
return priceWith8Decimals;
|
|
4057
4090
|
}
|
|
4058
4091
|
};
|
|
4092
|
+
var MAX_CLASSIFIED_PARENTS = 5;
|
|
4093
|
+
var CLASSIFICATION_CONCURRENCY = 2;
|
|
4094
|
+
var CONFIRMATION_SKEW_TOLERANCE = 3;
|
|
4095
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
4096
|
+
const results = new Array(items.length);
|
|
4097
|
+
let next = 0;
|
|
4098
|
+
const workers = Array.from(
|
|
4099
|
+
{ length: Math.min(limit, items.length) },
|
|
4100
|
+
async () => {
|
|
4101
|
+
while (true) {
|
|
4102
|
+
const i = next++;
|
|
4103
|
+
if (i >= items.length) {
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
results[i] = await fn(items[i]);
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
);
|
|
4110
|
+
await Promise.all(workers);
|
|
4111
|
+
return results;
|
|
4112
|
+
}
|
|
4113
|
+
function findOutputClaimMismatch(claimedUtxos, outputs, cleanVaultAddress) {
|
|
4114
|
+
for (const claim of claimedUtxos) {
|
|
4115
|
+
const output = outputs.find((o) => o.index === claim.vout);
|
|
4116
|
+
if (!output) {
|
|
4117
|
+
return `claimed output ${claim.txid}:${claim.vout} not present in parent tx`;
|
|
4118
|
+
}
|
|
4119
|
+
if (output.scriptpubkey_address !== cleanVaultAddress) {
|
|
4120
|
+
return `claimed output ${claim.txid}:${claim.vout} pays ${output.scriptpubkey_address ?? "<no address>"}, not the vault`;
|
|
4121
|
+
}
|
|
4122
|
+
if (output.value === void 0 || output.value !== claim.satoshis) {
|
|
4123
|
+
return `claimed output ${claim.txid}:${claim.vout} value ${output.value?.toString() ?? "<missing>"} != reported ${claim.satoshis.toString()}`;
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
return null;
|
|
4127
|
+
}
|
|
4128
|
+
function stripNetworkPrefix(address) {
|
|
4129
|
+
const prefixPattern = /^(REGTEST_|TESTNET_|MAINNET_)/;
|
|
4130
|
+
return address.replace(prefixPattern, "");
|
|
4131
|
+
}
|
|
4132
|
+
var VaultBalanceConsensus = class {
|
|
4133
|
+
/**
|
|
4134
|
+
* @param minConsensusProviders Policy floor (`policy.minConsensusProviders`,
|
|
4135
|
+
* remediation P1/#1). Both consensus passes THROW when fewer providers are
|
|
4136
|
+
* configured than the floor demands — below it the cross-checks cannot run
|
|
4137
|
+
* and neither trusting the primary (inflation) nor excluding UTXOs
|
|
4138
|
+
* (deflation → wrongful liquidation) is a safe direction. A floor of 1
|
|
4139
|
+
* (hardhat / single-provider sepolia) preserves the trust-primary /
|
|
4140
|
+
* exclude-unconfirmed baseline unchanged.
|
|
4141
|
+
*/
|
|
4142
|
+
constructor(primaryProvider, providerConfigs, minConsensusProviders) {
|
|
4143
|
+
this.primaryProvider = primaryProvider;
|
|
4144
|
+
this.providerConfigs = providerConfigs;
|
|
4145
|
+
this.minConsensusProviders = minConsensusProviders;
|
|
4146
|
+
this.readers = this.buildReaders();
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Fail-closed floor check shared by both consensus passes. Only floors above
|
|
4150
|
+
* 1 can bite: a floor of 1 with zero configs means "no esplora access
|
|
4151
|
+
* configured" (dev/test), where the documented pre-gate behaviour —
|
|
4152
|
+
* trust-primary confirmed set, exclude unconfirmed — remains correct.
|
|
4153
|
+
*/
|
|
4154
|
+
assertProviderFloor(pass) {
|
|
4155
|
+
const configured = this.providerConfigs?.length ?? 0;
|
|
4156
|
+
if (this.minConsensusProviders > 1 && configured < this.minConsensusProviders) {
|
|
4157
|
+
throw new Error(
|
|
4158
|
+
`${pass}: this chain requires at least ${this.minConsensusProviders} Bitcoin providers for consensus; ${configured} configured. Refusing to proceed without cross-check.`
|
|
4159
|
+
);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
/**
|
|
4163
|
+
* Build one data reader per configured provider (primary first). Index 0
|
|
4164
|
+
* reuses the injected primary instance; the rest are constructed from their
|
|
4165
|
+
* configs so cross-check reads hit the right endpoint. Null when no providers
|
|
4166
|
+
* are configured — both passes then fall back to primary-only behaviour.
|
|
4167
|
+
*/
|
|
4168
|
+
buildReaders() {
|
|
4169
|
+
const configs = this.providerConfigs;
|
|
4170
|
+
if (!configs || configs.length === 0) {
|
|
4171
|
+
return null;
|
|
4172
|
+
}
|
|
4173
|
+
return configs.map(
|
|
4174
|
+
(cfg, i) => i === 0 ? this.primaryProvider : new BitcoinDataProvider({
|
|
4175
|
+
providerUrl: cfg.providerUrl,
|
|
4176
|
+
auth: cfg.auth,
|
|
4177
|
+
...cfg.timeout ? { timeout: cfg.timeout } : {}
|
|
4178
|
+
})
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Confirmed-UTXO-set consensus across every registered provider (strict).
|
|
4183
|
+
*
|
|
4184
|
+
* The signed balance is dominated by the primary's confirmed UTXOs, but the
|
|
4185
|
+
* UTXO set — txid, vout, and value — is read only from the primary; the
|
|
4186
|
+
* `assertBitcoinBalanceConsensus` check the validators run compares
|
|
4187
|
+
* `chain_stats` totals, not individual outpoints. So a compromised primary
|
|
4188
|
+
* can add a fabricated confirmed UTXO, or inflate a real one's value, and the
|
|
4189
|
+
* balance consensus still passes (it serves honest `chain_stats`). This
|
|
4190
|
+
* closes that: each UTXO the primary reports at or above `minConfirmations`
|
|
4191
|
+
* must be present with the EXACT same value on every registered provider.
|
|
4192
|
+
*
|
|
4193
|
+
* Semantics:
|
|
4194
|
+
* - Absent on any provider, or value mismatch → THROW (fail-closed). Confirmed
|
|
4195
|
+
* transactions are fully propagated, so absence is unambiguous forgery, not
|
|
4196
|
+
* a race.
|
|
4197
|
+
* - Present + value-matched everywhere: the UTXO counts as confirmed only if
|
|
4198
|
+
* it meets `minConfirmations` on ALL providers (gated on the minimum
|
|
4199
|
+
* reported depth). A UTXO just past the boundary on the primary but a block
|
|
4200
|
+
* behind on a lagging provider is DEMOTED — excluded until it matures —
|
|
4201
|
+
* rather than signed over. A depth spread beyond CONFIRMATION_SKEW_TOLERANCE
|
|
4202
|
+
* is not benign skew but a provider lying about depth (to inflate a shallow
|
|
4203
|
+
* UTXO past the gate, or deflate a deep one into a wrongful liquidation) →
|
|
4204
|
+
* THROW.
|
|
4205
|
+
* - SYMMETRIC (finding #6): a confirmed outpoint any secondary reports that
|
|
4206
|
+
* the primary omits entirely, reports with a different value, or reports
|
|
4207
|
+
* at a depth more than the skew tolerance BELOW the secondary's → THROW.
|
|
4208
|
+
* Without this, a compromised primary deflates the balance by omission and
|
|
4209
|
+
* forces a wrongful liquidation.
|
|
4210
|
+
*
|
|
4211
|
+
* Below `minConsensusProviders` this THROWS (P1/#1). With a floor of 1 and
|
|
4212
|
+
* 0/1 providers configured, no cross-check is possible and the primary's
|
|
4213
|
+
* confirmed UTXOs are returned unchanged (behaviour identical to before
|
|
4214
|
+
* this gate).
|
|
4215
|
+
*
|
|
4216
|
+
* Cross-checking `chain_stats` totals against the confirmed-UTXO sum was
|
|
4217
|
+
* considered and rejected: `getBalance` reads mempool-blind `chain_stats`
|
|
4218
|
+
* while `getUTXOSet` removes mempool-spent outputs, so the two legitimately
|
|
4219
|
+
* diverge during any pending spend — precisely the self-change window — and
|
|
4220
|
+
* that comparison would false-fail there. Outpoint-set consensus asserts
|
|
4221
|
+
* agreement only on confirmed outpoints, which the mempool does not touch.
|
|
4222
|
+
*
|
|
4223
|
+
* @returns Set of "txid:vout" keys admitted as confirmed-with-consensus.
|
|
4224
|
+
*/
|
|
4225
|
+
async confirmedUtxoConsensus(primaryUtxos, vaultAddress, minConfirmations) {
|
|
4226
|
+
const providerConfigs = this.providerConfigs;
|
|
4227
|
+
const readers = this.readers;
|
|
4228
|
+
this.assertProviderFloor("confirmedUtxoConsensus");
|
|
4229
|
+
const confirmedKeys = /* @__PURE__ */ new Set();
|
|
4230
|
+
const primaryConfirmed = primaryUtxos.filter(
|
|
4231
|
+
(utxo) => utxo.confirmations >= minConfirmations
|
|
4232
|
+
);
|
|
4233
|
+
for (const utxo of primaryConfirmed) {
|
|
4234
|
+
confirmedKeys.add(`${utxo.txid}:${utxo.vout}`);
|
|
4235
|
+
}
|
|
4236
|
+
if (!providerConfigs || providerConfigs.length <= 1 || !readers) {
|
|
4237
|
+
return confirmedKeys;
|
|
4238
|
+
}
|
|
4239
|
+
const providerName = (i) => providerConfigs[i].name || providerConfigs[i].providerUrl;
|
|
4240
|
+
const settled = await Promise.allSettled(
|
|
4241
|
+
readers.slice(1).map((reader) => reader.getUTXOSet(vaultAddress, minConfirmations))
|
|
4242
|
+
);
|
|
4243
|
+
const failures = settled.map((r, i) => ({ r, name: providerName(i + 1) })).filter(
|
|
4244
|
+
(x) => x.r.status === "rejected"
|
|
4245
|
+
).map(
|
|
4246
|
+
({ r, name }) => `${name} \u2192 ${r.reason?.message ?? String(r.reason)}`
|
|
4247
|
+
);
|
|
4248
|
+
if (failures.length > 0) {
|
|
4249
|
+
throw new Error(
|
|
4250
|
+
`Confirmed UTXO set read failed for ${vaultAddress} on ${failures.length}/${providerConfigs.length - 1} secondary provider(s): ${failures.join("; ")}`
|
|
4251
|
+
);
|
|
4252
|
+
}
|
|
4253
|
+
const secondaryMaps = settled.map((r) => {
|
|
4254
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
4255
|
+
for (const utxo of r.value.utxos) {
|
|
4256
|
+
map2.set(`${utxo.txid}:${utxo.vout}`, {
|
|
4257
|
+
value: utxo.satoshis,
|
|
4258
|
+
confirmations: utxo.confirmations
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
return map2;
|
|
4262
|
+
});
|
|
4263
|
+
const demoted = [];
|
|
4264
|
+
for (const utxo of primaryConfirmed) {
|
|
4265
|
+
const key = `${utxo.txid}:${utxo.vout}`;
|
|
4266
|
+
let minConfs = utxo.confirmations;
|
|
4267
|
+
let maxConfs = utxo.confirmations;
|
|
4268
|
+
for (let s = 0; s < secondaryMaps.length; s++) {
|
|
4269
|
+
const entry = secondaryMaps[s].get(key);
|
|
4270
|
+
const name = providerName(s + 1);
|
|
4271
|
+
if (!entry) {
|
|
4272
|
+
throw new Error(
|
|
4273
|
+
`Confirmed UTXO ${key} present on ${providerName(0)} but absent on ${name}; refusing to sign (possible fabricated UTXO).`
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
if (entry.value !== utxo.satoshis) {
|
|
4277
|
+
throw new Error(
|
|
4278
|
+
`Confirmed UTXO ${key} value mismatch: ${providerName(0)}=${utxo.satoshis.toString()} vs ${name}=${entry.value.toString()}; refusing to sign.`
|
|
4279
|
+
);
|
|
4280
|
+
}
|
|
4281
|
+
if (entry.confirmations < minConfs)
|
|
4282
|
+
minConfs = entry.confirmations;
|
|
4283
|
+
if (entry.confirmations > maxConfs)
|
|
4284
|
+
maxConfs = entry.confirmations;
|
|
4285
|
+
}
|
|
4286
|
+
if (maxConfs - minConfs > CONFIRMATION_SKEW_TOLERANCE) {
|
|
4287
|
+
throw new Error(
|
|
4288
|
+
`Confirmed UTXO ${key} confirmation spread ${maxConfs - minConfs} exceeds tolerance ${CONFIRMATION_SKEW_TOLERANCE} (min ${minConfs}, max ${maxConfs}); providers disagree on depth, refusing to sign.`
|
|
4289
|
+
);
|
|
4290
|
+
}
|
|
4291
|
+
if (minConfs < minConfirmations) {
|
|
4292
|
+
confirmedKeys.delete(key);
|
|
4293
|
+
demoted.push(key);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
if (demoted.length > 0) {
|
|
4297
|
+
litDebugLog(
|
|
4298
|
+
`[VaultBalance] confirmed-consensus: ${demoted.length} UTXO(s) demoted pending provider tip skew (excluded until matured): ${demoted.join(", ")}`
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
const primaryByKey = new Map(
|
|
4302
|
+
primaryUtxos.map((utxo) => [`${utxo.txid}:${utxo.vout}`, utxo])
|
|
4303
|
+
);
|
|
4304
|
+
for (let s = 0; s < secondaryMaps.length; s++) {
|
|
4305
|
+
const name = providerName(s + 1);
|
|
4306
|
+
for (const [key, entry] of secondaryMaps[s]) {
|
|
4307
|
+
if (entry.confirmations < minConfirmations)
|
|
4308
|
+
continue;
|
|
4309
|
+
const primaryUtxo = primaryByKey.get(key);
|
|
4310
|
+
if (!primaryUtxo) {
|
|
4311
|
+
throw new Error(
|
|
4312
|
+
`Confirmed UTXO ${key} present on ${name} (${entry.confirmations} confs) but absent on ${providerName(0)}; refusing to sign (possible omitted UTXO / balance deflation \u2014 may be transient during an in-flight vault spend).`
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
if (primaryUtxo.satoshis !== entry.value) {
|
|
4316
|
+
throw new Error(
|
|
4317
|
+
`Confirmed UTXO ${key} value mismatch: ${providerName(0)}=${primaryUtxo.satoshis.toString()} vs ${name}=${entry.value.toString()}; refusing to sign.`
|
|
4318
|
+
);
|
|
4319
|
+
}
|
|
4320
|
+
if (entry.confirmations - primaryUtxo.confirmations > CONFIRMATION_SKEW_TOLERANCE) {
|
|
4321
|
+
throw new Error(
|
|
4322
|
+
`Confirmed UTXO ${key} depth disagreement: ${providerName(0)}=${primaryUtxo.confirmations} vs ${name}=${entry.confirmations} exceeds tolerance ${CONFIRMATION_SKEW_TOLERANCE}; the primary may be under-reporting depth to suppress collateral, refusing to sign.`
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
return confirmedKeys;
|
|
4328
|
+
}
|
|
4329
|
+
/**
|
|
4330
|
+
* Classify unconfirmed UTXOs as protocol self-change.
|
|
4331
|
+
*
|
|
4332
|
+
* A UTXO below minConfirmations counts as confirmed-equivalent ONLY when the
|
|
4333
|
+
* transaction that created it spends exclusively vault-owned, confirmed
|
|
4334
|
+
* outputs. Such a transaction can only have been signed by the vault's PKP,
|
|
4335
|
+
* so no third party can RBF-replace or double-spend it — the reversal risk
|
|
4336
|
+
* the confirmation gate exists to close does not apply. Any non-vault input,
|
|
4337
|
+
* unconfirmed funding tx, or coinbase input disqualifies the UTXO
|
|
4338
|
+
* (fail-safe: exclusion, never inclusion).
|
|
4339
|
+
*
|
|
4340
|
+
* Multi-provider consensus (audit C-1/C-2 parity): the parent-tx proof is
|
|
4341
|
+
* verified independently against EVERY configured provider and admitted
|
|
4342
|
+
* only on unanimity. The confirmed-balance consensus the validators run
|
|
4343
|
+
* covers `chain_stats` only, so without this the primary provider alone
|
|
4344
|
+
* could fabricate an unconfirmed UTXO plus a self-consistent parent proof
|
|
4345
|
+
* and inflate the signed balance. Any provider failure or verdict
|
|
4346
|
+
* disagreement THROWS (fail-closed) — exclusion on a suppressed proof
|
|
4347
|
+
* would deflate the balance and open a wrongful-liquidation window, so
|
|
4348
|
+
* neither direction may proceed without agreement.
|
|
4349
|
+
*
|
|
4350
|
+
* The same per-provider `/tx` fetch also returns the parent's outputs, and
|
|
4351
|
+
* every provider must corroborate each claimed UTXO (index, vault address,
|
|
4352
|
+
* exact value) before admission — the UTXO list itself comes only from the
|
|
4353
|
+
* primary, so an unchecked claim would let it attach a fabricated or
|
|
4354
|
+
* inflated output to a genuine self-spend.
|
|
4355
|
+
*
|
|
4356
|
+
* Fetch cost: zero when every UTXO is confirmed (the common case); otherwise
|
|
4357
|
+
* per provider: one tx lookup per classified parent (vins + vouts in one
|
|
4358
|
+
* response), plus per distinct funding tx a confirmation check and — only
|
|
4359
|
+
* when the provider omits vin prevout data — an output lookup. Classified
|
|
4360
|
+
* parents are capped at MAX_CLASSIFIED_PARENTS (largest claimed value
|
|
4361
|
+
* first, deterministic) and evaluated CLASSIFICATION_CONCURRENCY at a time,
|
|
4362
|
+
* so third-party dust spam cannot amplify into provider rate-limiting and
|
|
4363
|
+
* an action-wide outage.
|
|
4364
|
+
*
|
|
4365
|
+
* See docs/vault-balance-confirmed-gate-plan-2026-07-14.md §3.1.
|
|
4366
|
+
*
|
|
4367
|
+
* @param utxos - Full UTXO set for the vault address
|
|
4368
|
+
* @param vaultAddress - Bitcoin address of the vault (may carry network prefix)
|
|
4369
|
+
* @param minConfirmations - Confirmation threshold for the chain
|
|
4370
|
+
* @returns Set of "txid:vout" keys for UTXOs verified as self-change
|
|
4371
|
+
*/
|
|
4372
|
+
async classifySelfChange(utxos, vaultAddress, minConfirmations) {
|
|
4373
|
+
const providerConfigs = this.providerConfigs;
|
|
4374
|
+
const readers = this.readers;
|
|
4375
|
+
const selfChange = /* @__PURE__ */ new Set();
|
|
4376
|
+
const unconfirmed = utxos.filter(
|
|
4377
|
+
(utxo) => utxo.confirmations < minConfirmations
|
|
4378
|
+
);
|
|
4379
|
+
if (unconfirmed.length === 0) {
|
|
4380
|
+
return selfChange;
|
|
4381
|
+
}
|
|
4382
|
+
this.assertProviderFloor("classifySelfChange");
|
|
4383
|
+
if (!providerConfigs || providerConfigs.length === 0 || !readers) {
|
|
4384
|
+
litDebugLog(
|
|
4385
|
+
`[VaultBalance] ${unconfirmed.length} unconfirmed UTXO(s) excluded: no bitcoinProviderConfigs for self-change classification`
|
|
4386
|
+
);
|
|
4387
|
+
return selfChange;
|
|
4388
|
+
}
|
|
4389
|
+
const cleanVaultAddress = stripNetworkPrefix(vaultAddress);
|
|
4390
|
+
const claimsByParent = /* @__PURE__ */ new Map();
|
|
4391
|
+
for (const utxo of unconfirmed) {
|
|
4392
|
+
const claims = claimsByParent.get(utxo.txid) ?? [];
|
|
4393
|
+
claims.push(utxo);
|
|
4394
|
+
claimsByParent.set(utxo.txid, claims);
|
|
4395
|
+
}
|
|
4396
|
+
const claimedSum = (txid) => claimsByParent.get(txid).reduce((sum, utxo) => sum + utxo.satoshis, 0n);
|
|
4397
|
+
const rankedParents = [...claimsByParent.keys()].sort((a, b) => {
|
|
4398
|
+
const diff = claimedSum(b) - claimedSum(a);
|
|
4399
|
+
if (diff !== 0n) {
|
|
4400
|
+
return diff > 0n ? 1 : -1;
|
|
4401
|
+
}
|
|
4402
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
4403
|
+
});
|
|
4404
|
+
const parentTxids = rankedParents.slice(0, MAX_CLASSIFIED_PARENTS);
|
|
4405
|
+
const skippedParents = rankedParents.length - parentTxids.length;
|
|
4406
|
+
if (skippedParents > 0) {
|
|
4407
|
+
litDebugLog(
|
|
4408
|
+
`[VaultBalance] self-change: ${skippedParents} parent tx(s) beyond the ${MAX_CLASSIFIED_PARENTS}-parent classification budget excluded (mempool-spam bound; largest claimed value classified first)`
|
|
4409
|
+
);
|
|
4410
|
+
}
|
|
4411
|
+
const verdicts = await mapWithConcurrency(
|
|
4412
|
+
parentTxids,
|
|
4413
|
+
CLASSIFICATION_CONCURRENCY,
|
|
4414
|
+
async (parentTxid) => ({
|
|
4415
|
+
parentTxid,
|
|
4416
|
+
verified: await this.verifyParentIsSelfSpendConsensus(
|
|
4417
|
+
parentTxid,
|
|
4418
|
+
claimsByParent.get(parentTxid),
|
|
4419
|
+
cleanVaultAddress,
|
|
4420
|
+
minConfirmations,
|
|
4421
|
+
providerConfigs,
|
|
4422
|
+
readers
|
|
4423
|
+
)
|
|
4424
|
+
})
|
|
4425
|
+
);
|
|
4426
|
+
const verifiedParents = new Set(
|
|
4427
|
+
verdicts.filter((v) => v.verified).map((v) => v.parentTxid)
|
|
4428
|
+
);
|
|
4429
|
+
for (const utxo of unconfirmed) {
|
|
4430
|
+
if (verifiedParents.has(utxo.txid)) {
|
|
4431
|
+
selfChange.add(`${utxo.txid}:${utxo.vout}`);
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
if (verifiedParents.size > 0) {
|
|
4435
|
+
litDebugLog(
|
|
4436
|
+
`[VaultBalance] self-change: ${selfChange.size}/${unconfirmed.length} unconfirmed UTXO(s) verified as protocol change by ${providerConfigs.length} provider(s)`
|
|
4437
|
+
);
|
|
4438
|
+
}
|
|
4439
|
+
return selfChange;
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Run the self-spend proof against every configured provider and require
|
|
4443
|
+
* unanimity. Fail-closed: a provider request failure or a verdict
|
|
4444
|
+
* disagreement throws and aborts the balance calculation — see
|
|
4445
|
+
* classifySelfChange for why exclusion is not a safe fallback here.
|
|
4446
|
+
*
|
|
4447
|
+
* When the parent IS a unanimous self-spend, every provider's view of the
|
|
4448
|
+
* parent's OUTPUTS must additionally corroborate the primary's claimed
|
|
4449
|
+
* UTXOs (index exists, pays the vault, exact value). Without this, a
|
|
4450
|
+
* compromised primary could attach a fabricated or value-inflated output
|
|
4451
|
+
* to a genuine self-spend the honest providers happily verify.
|
|
4452
|
+
*
|
|
4453
|
+
* @returns True when every provider verified the parent and corroborated
|
|
4454
|
+
* the claimed outputs, false when every provider rejected the
|
|
4455
|
+
* parent (genuine third-party deposit)
|
|
4456
|
+
*/
|
|
4457
|
+
async verifyParentIsSelfSpendConsensus(parentTxid, claimedUtxos, cleanVaultAddress, minConfirmations, providerConfigs, readers) {
|
|
4458
|
+
const providerName = (i) => providerConfigs[i].name || providerConfigs[i].providerUrl;
|
|
4459
|
+
const settled = await Promise.allSettled(
|
|
4460
|
+
providerConfigs.map(
|
|
4461
|
+
(cfg, i) => this.evaluateParentForProvider(
|
|
4462
|
+
parentTxid,
|
|
4463
|
+
claimedUtxos,
|
|
4464
|
+
cleanVaultAddress,
|
|
4465
|
+
minConfirmations,
|
|
4466
|
+
readers[i],
|
|
4467
|
+
cfg
|
|
4468
|
+
)
|
|
4469
|
+
)
|
|
4470
|
+
);
|
|
4471
|
+
const failures = settled.map((r, i) => ({ r, name: providerName(i) })).filter(
|
|
4472
|
+
(x) => x.r.status === "rejected"
|
|
4473
|
+
).map(
|
|
4474
|
+
({ r, name }) => `${name} \u2192 ${r.reason?.message ?? String(r.reason)}`
|
|
4475
|
+
);
|
|
4476
|
+
if (failures.length > 0) {
|
|
4477
|
+
throw new Error(
|
|
4478
|
+
`Self-change classification failed for tx ${parentTxid} on ${failures.length}/${providerConfigs.length} provider(s): ${failures.join("; ")}`
|
|
4479
|
+
);
|
|
4480
|
+
}
|
|
4481
|
+
const evaluations = settled.map(
|
|
4482
|
+
(r) => r.value
|
|
4483
|
+
);
|
|
4484
|
+
const votes = evaluations.map((e) => e.selfSpend);
|
|
4485
|
+
const disagreements = votes.map((vote, i) => ({ vote, name: providerName(i) })).filter((x) => x.vote !== votes[0]).map(
|
|
4486
|
+
(x) => `${providerName(0)}=${votes[0]} vs ${x.name}=${x.vote}`
|
|
4487
|
+
);
|
|
4488
|
+
if (disagreements.length > 0) {
|
|
4489
|
+
throw new Error(
|
|
4490
|
+
`Self-change consensus mismatch for tx ${parentTxid}: ${disagreements.join("; ")}. A provider may be compromised or out of sync; refusing to sign.`
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
if (!votes[0]) {
|
|
4494
|
+
return false;
|
|
4495
|
+
}
|
|
4496
|
+
const outputMismatches = evaluations.map((e, i) => e.outputMismatch ? `${providerName(i)} \u2192 ${e.outputMismatch}` : null).filter((m) => m !== null);
|
|
4497
|
+
if (outputMismatches.length > 0) {
|
|
4498
|
+
throw new Error(
|
|
4499
|
+
`Self-change output claim mismatch for tx ${parentTxid}: ${outputMismatches.join("; ")}. The reported UTXO set contradicts the parent transaction; refusing to sign.`
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
return true;
|
|
4503
|
+
}
|
|
4504
|
+
/**
|
|
4505
|
+
* Single provider's evaluation of a parent tx: the self-spend proof plus —
|
|
4506
|
+
* when the proof holds — corroboration of the claimed outputs against this
|
|
4507
|
+
* provider's view of the parent's vout set. The output check is skipped for
|
|
4508
|
+
* non-self-spend parents (their UTXOs are excluded regardless, and a
|
|
4509
|
+
* third-party deposit's values are not ours to police).
|
|
4510
|
+
*/
|
|
4511
|
+
async evaluateParentForProvider(parentTxid, claimedUtxos, cleanVaultAddress, minConfirmations, reader, providerConfig) {
|
|
4512
|
+
const parentTx = await this.fetchParentTransaction(
|
|
4513
|
+
parentTxid,
|
|
4514
|
+
providerConfig
|
|
4515
|
+
);
|
|
4516
|
+
if (!parentTx || parentTx.inputs.length === 0) {
|
|
4517
|
+
return { selfSpend: false, outputMismatch: null };
|
|
4518
|
+
}
|
|
4519
|
+
const selfSpend = await this.verifyInputsAreSelfSpend(
|
|
4520
|
+
parentTx.inputs,
|
|
4521
|
+
cleanVaultAddress,
|
|
4522
|
+
minConfirmations,
|
|
4523
|
+
reader
|
|
4524
|
+
);
|
|
4525
|
+
const outputMismatch = selfSpend ? findOutputClaimMismatch(claimedUtxos, parentTx.outputs, cleanVaultAddress) : null;
|
|
4526
|
+
return { selfSpend, outputMismatch };
|
|
4527
|
+
}
|
|
4528
|
+
/**
|
|
4529
|
+
* Verify that a transaction's inputs spend exclusively vault-owned,
|
|
4530
|
+
* confirmed outputs, using a single provider's view of the chain.
|
|
4531
|
+
*
|
|
4532
|
+
* @param inputs - Parsed inputs of the parent transaction
|
|
4533
|
+
* @param cleanVaultAddress - Vault address with network prefix stripped
|
|
4534
|
+
* @param minConfirmations - Required depth for every funding transaction
|
|
4535
|
+
* @param reader - Data provider for funding-tx lookups (same provider the inputs came from)
|
|
4536
|
+
* @returns True only when every input is a confirmed, vault-owned outpoint
|
|
4537
|
+
*/
|
|
4538
|
+
async verifyInputsAreSelfSpend(inputs, cleanVaultAddress, minConfirmations, reader) {
|
|
4539
|
+
const fundingTxids = [...new Set(inputs.map((input) => input.txid))];
|
|
4540
|
+
const outputsByFundingTx = /* @__PURE__ */ new Map();
|
|
4541
|
+
const needsOutputLookup = fundingTxids.filter(
|
|
4542
|
+
(fundingTxid) => inputs.some(
|
|
4543
|
+
(input) => input.txid === fundingTxid && input.prevoutAddress === void 0
|
|
4544
|
+
)
|
|
4545
|
+
);
|
|
4546
|
+
await Promise.all(
|
|
4547
|
+
needsOutputLookup.map(async (fundingTxid) => {
|
|
4548
|
+
outputsByFundingTx.set(
|
|
4549
|
+
fundingTxid,
|
|
4550
|
+
await reader.getTransactionOutputs(fundingTxid)
|
|
4551
|
+
);
|
|
4552
|
+
})
|
|
4553
|
+
);
|
|
4554
|
+
for (const input of inputs) {
|
|
4555
|
+
let ownerAddress = input.prevoutAddress;
|
|
4556
|
+
if (ownerAddress === void 0) {
|
|
4557
|
+
const outputs = outputsByFundingTx.get(input.txid);
|
|
4558
|
+
ownerAddress = outputs?.find(
|
|
4559
|
+
(output) => output.index === input.vout
|
|
4560
|
+
)?.scriptpubkey_address;
|
|
4561
|
+
}
|
|
4562
|
+
if (ownerAddress !== cleanVaultAddress) {
|
|
4563
|
+
return false;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
const fundingStatuses = await Promise.all(
|
|
4567
|
+
fundingTxids.map((fundingTxid) => reader.getTransaction(fundingTxid))
|
|
4568
|
+
);
|
|
4569
|
+
for (const status of fundingStatuses) {
|
|
4570
|
+
if (!status || status.confirmations < minConfirmations) {
|
|
4571
|
+
return false;
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
return true;
|
|
4575
|
+
}
|
|
4576
|
+
/**
|
|
4577
|
+
* Fetch a parent transaction's inputs (vins) and outputs (vouts) via Esplora.
|
|
4578
|
+
*
|
|
4579
|
+
* A module-private fetch (not a `BitcoinDataProvider` method) so editing this
|
|
4580
|
+
* consensus logic does not rebundle the shared provider module's other
|
|
4581
|
+
* importers — see this file's header for the ripple-set constraint.
|
|
4582
|
+
*
|
|
4583
|
+
* @param txid - Transaction ID
|
|
4584
|
+
* @param providerConfig - Esplora endpoint to query (one of the configured
|
|
4585
|
+
* consensus providers)
|
|
4586
|
+
* @returns Parsed inputs + outputs, or null ONLY when the tx is provably not
|
|
4587
|
+
* a self-spend candidate (coinbase-funded). Provider inability —
|
|
4588
|
+
* 404, malformed vin rows, missing vin array — THROWS (review
|
|
4589
|
+
* 2026-07-15 P1): the parent of a UTXO the primary lists must
|
|
4590
|
+
* exist, so "can't fetch it" is a provider failure, and treating it
|
|
4591
|
+
* as a false self-change vote would silently deflate collateral
|
|
4592
|
+
* (wrongful-liquidation direction) if every provider failed the
|
|
4593
|
+
* same way.
|
|
4594
|
+
*/
|
|
4595
|
+
async fetchParentTransaction(txid, providerConfig) {
|
|
4596
|
+
if (!providerConfig.providerUrl) {
|
|
4597
|
+
return null;
|
|
4598
|
+
}
|
|
4599
|
+
const timeout = providerConfig.timeout || 15e3;
|
|
4600
|
+
const controller = new AbortController();
|
|
4601
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
4602
|
+
try {
|
|
4603
|
+
const response = await fetch(`${providerConfig.providerUrl}/tx/${txid}`, {
|
|
4604
|
+
signal: controller.signal,
|
|
4605
|
+
headers: await authHeaders(providerConfig.auth, timeout)
|
|
4606
|
+
});
|
|
4607
|
+
if (!response.ok) {
|
|
4608
|
+
if (response.status === 404) {
|
|
4609
|
+
throw new Error(
|
|
4610
|
+
`parent tx ${txid} not found on this provider \u2014 cannot verify the self-change proof`
|
|
4611
|
+
);
|
|
4612
|
+
}
|
|
4613
|
+
throw new Error(
|
|
4614
|
+
`Bitcoin provider error: ${response.status} ${response.statusText}`
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
const data = await response.json();
|
|
4618
|
+
if (!data.vin || !Array.isArray(data.vin) || data.vin.length === 0) {
|
|
4619
|
+
throw new Error(
|
|
4620
|
+
`provider returned tx ${txid} without a vin array \u2014 malformed response`
|
|
4621
|
+
);
|
|
4622
|
+
}
|
|
4623
|
+
const inputs = [];
|
|
4624
|
+
for (const vin of data.vin) {
|
|
4625
|
+
if (vin.is_coinbase) {
|
|
4626
|
+
return null;
|
|
4627
|
+
}
|
|
4628
|
+
if (vin.txid === void 0 || vin.vout === void 0) {
|
|
4629
|
+
throw new Error(
|
|
4630
|
+
`provider returned tx ${txid} with a malformed vin row (missing txid/vout)`
|
|
4631
|
+
);
|
|
4632
|
+
}
|
|
4633
|
+
inputs.push({
|
|
4634
|
+
txid: vin.txid,
|
|
4635
|
+
vout: vin.vout,
|
|
4636
|
+
prevoutAddress: vin.prevout?.scriptpubkey_address
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
const outputs = Array.isArray(data.vout) ? data.vout.map((vout, index) => ({
|
|
4640
|
+
index,
|
|
4641
|
+
scriptpubkey_address: vout?.scriptpubkey_address,
|
|
4642
|
+
value: typeof vout?.value === "number" ? BigInt(vout.value) : void 0
|
|
4643
|
+
})) : [];
|
|
4644
|
+
return { inputs, outputs };
|
|
4645
|
+
} catch (error) {
|
|
4646
|
+
if (isAbortError(error)) {
|
|
4647
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
4648
|
+
}
|
|
4649
|
+
throw error;
|
|
4650
|
+
} finally {
|
|
4651
|
+
clearTimeout(timeoutId);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
};
|
|
4059
4655
|
var VaultBalanceModule = class {
|
|
4060
4656
|
constructor(config) {
|
|
4657
|
+
if (!Number.isInteger(config.minConfirmations) || config.minConfirmations < 1) {
|
|
4658
|
+
throw new Error(
|
|
4659
|
+
`VaultBalanceModule: minConfirmations must be a positive integer (got ${config.minConfirmations}). Pass the policy-derived balance-confirmation threshold \u2014 there is no default.`
|
|
4660
|
+
);
|
|
4661
|
+
}
|
|
4061
4662
|
this.config = config;
|
|
4062
4663
|
this.bitcoinProvider = config.bitcoinProvider;
|
|
4063
4664
|
}
|
|
4064
4665
|
/**
|
|
4065
|
-
* Calculate balance (
|
|
4666
|
+
* Calculate balance (arithmetic only - no UTXO validation)
|
|
4066
4667
|
*
|
|
4067
|
-
*
|
|
4068
|
-
* Uses simple arithmetic: Total Balance - Authorized Balance = Available Balance
|
|
4668
|
+
* Balance-only calculation: Total Balance - Authorized Balance = Available.
|
|
4069
4669
|
*
|
|
4070
|
-
*
|
|
4071
|
-
* -
|
|
4072
|
-
*
|
|
4670
|
+
* ⚠️ SECURITY (remediation P1/#5): this result skips UTXO validation, the
|
|
4671
|
+
* confirmed-consensus gate, and self-change proofing. It must NEVER feed a
|
|
4672
|
+
* signed BalanceConfirmation or any authorization decision. No production
|
|
4673
|
+
* action calls it (the `skipUTXOValidation` fast path that used to route
|
|
4674
|
+
* here was deleted); it exists for read-only diagnostics and tests. Adding
|
|
4675
|
+
* a production caller requires Master sign-off.
|
|
4073
4676
|
*
|
|
4074
4677
|
* @param positionId - The position ID to check
|
|
4075
4678
|
* @param vaultAddress - Bitcoin address of the vault
|
|
@@ -4116,17 +4719,18 @@ ${auth.clientId}`;
|
|
|
4116
4719
|
* 1. Authorized transaction not found on network (not broadcasted)
|
|
4117
4720
|
* 2. Authorized transaction has insufficient confirmations (network updating)
|
|
4118
4721
|
*
|
|
4722
|
+
* NOTE: the `skipUTXOValidation` fast path was removed (remediation P1/#5).
|
|
4723
|
+
* Every trusted-balance read runs full UTXO validation + consensus; a
|
|
4724
|
+
* balance that skipped validation must never feed a signed
|
|
4725
|
+
* BalanceConfirmation.
|
|
4726
|
+
*
|
|
4119
4727
|
* @param positionId - The position ID to check
|
|
4120
4728
|
* @param vaultAddress - Bitcoin address of the vault
|
|
4121
|
-
* @param skipUTXOValidation - If true, use fast balance-only path (skips UTXO validation)
|
|
4122
4729
|
* @returns Complete breakdown of vault balance
|
|
4123
4730
|
* @throws Error if authorized spends are in invalid state
|
|
4124
4731
|
*/
|
|
4125
|
-
async calculateTrustedBalance(positionId, vaultAddress
|
|
4126
|
-
|
|
4127
|
-
return await this.calculateBalance(positionId, vaultAddress);
|
|
4128
|
-
}
|
|
4129
|
-
const minConfirmations = this.config.minConfirmations || 6;
|
|
4732
|
+
async calculateTrustedBalance(positionId, vaultAddress) {
|
|
4733
|
+
const minConfirmations = this.config.minConfirmations;
|
|
4130
4734
|
const utxoSet = await this.bitcoinProvider.getUTXOSet(
|
|
4131
4735
|
vaultAddress,
|
|
4132
4736
|
minConfirmations
|
|
@@ -4146,8 +4750,25 @@ ${auth.clientId}`;
|
|
|
4146
4750
|
(sum, spend) => sum + spend.satoshis,
|
|
4147
4751
|
0n
|
|
4148
4752
|
);
|
|
4753
|
+
const consensus = new VaultBalanceConsensus(
|
|
4754
|
+
this.bitcoinProvider,
|
|
4755
|
+
this.config.bitcoinProviderConfigs,
|
|
4756
|
+
// Absent ⇒ 1: dev/test constructions without a policy keep the
|
|
4757
|
+
// single-provider baseline; validators pass policy.minConsensusProviders.
|
|
4758
|
+
this.config.minConsensusProviders ?? 1
|
|
4759
|
+
);
|
|
4760
|
+
const confirmedConsensusKeys = await consensus.confirmedUtxoConsensus(
|
|
4761
|
+
utxoSet.utxos,
|
|
4762
|
+
vaultAddress,
|
|
4763
|
+
minConfirmations
|
|
4764
|
+
);
|
|
4765
|
+
const selfChangeKeys = await consensus.classifySelfChange(
|
|
4766
|
+
utxoSet.utxos,
|
|
4767
|
+
vaultAddress,
|
|
4768
|
+
minConfirmations
|
|
4769
|
+
);
|
|
4149
4770
|
const availableUTXOs = utxoSet.utxos.filter(
|
|
4150
|
-
(utxo) => !this.isUTXOAuthorized(utxo, authorizedSpends)
|
|
4771
|
+
(utxo) => (confirmedConsensusKeys.has(`${utxo.txid}:${utxo.vout}`) || selfChangeKeys.has(`${utxo.txid}:${utxo.vout}`)) && !this.isUTXOAuthorized(utxo, authorizedSpends)
|
|
4151
4772
|
);
|
|
4152
4773
|
const availableBalance = availableUTXOs.reduce(
|
|
4153
4774
|
(sum, utxo) => sum + utxo.satoshis,
|
|
@@ -4379,14 +5000,84 @@ ${auth.clientId}`;
|
|
|
4379
5000
|
function createVaultBalanceModule(config) {
|
|
4380
5001
|
return new VaultBalanceModule(config);
|
|
4381
5002
|
}
|
|
5003
|
+
var POLICY_BY_CHAIN = {
|
|
5004
|
+
[
|
|
5005
|
+
"hardhat"
|
|
5006
|
+
/* HARDHAT */
|
|
5007
|
+
]: {
|
|
5008
|
+
chain: "hardhat",
|
|
5009
|
+
chainId: CHAIN_TO_CHAIN_ID[
|
|
5010
|
+
"hardhat"
|
|
5011
|
+
/* HARDHAT */
|
|
5012
|
+
],
|
|
5013
|
+
bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
|
|
5014
|
+
"hardhat"
|
|
5015
|
+
/* HARDHAT */
|
|
5016
|
+
],
|
|
5017
|
+
allowArbitraryBitcoinProvider: true,
|
|
5018
|
+
allowArbitraryEvmRpc: true,
|
|
5019
|
+
allowDevSinglePriceProvider: true,
|
|
5020
|
+
minBitcoinBalanceConfirmations: 1,
|
|
5021
|
+
minBitcoinSpendConfirmations: 1,
|
|
5022
|
+
minConsensusProviders: 1
|
|
5023
|
+
},
|
|
5024
|
+
[
|
|
5025
|
+
"sepolia"
|
|
5026
|
+
/* SEPOLIA */
|
|
5027
|
+
]: {
|
|
5028
|
+
chain: "sepolia",
|
|
5029
|
+
chainId: CHAIN_TO_CHAIN_ID[
|
|
5030
|
+
"sepolia"
|
|
5031
|
+
/* SEPOLIA */
|
|
5032
|
+
],
|
|
5033
|
+
bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
|
|
5034
|
+
"sepolia"
|
|
5035
|
+
/* SEPOLIA */
|
|
5036
|
+
],
|
|
5037
|
+
allowArbitraryBitcoinProvider: false,
|
|
5038
|
+
allowArbitraryEvmRpc: false,
|
|
5039
|
+
allowDevSinglePriceProvider: false,
|
|
5040
|
+
minBitcoinBalanceConfirmations: 1,
|
|
5041
|
+
minBitcoinSpendConfirmations: 1,
|
|
5042
|
+
minConsensusProviders: 1
|
|
5043
|
+
},
|
|
5044
|
+
[
|
|
5045
|
+
"ethereum"
|
|
5046
|
+
/* ETHEREUM */
|
|
5047
|
+
]: {
|
|
5048
|
+
chain: "ethereum",
|
|
5049
|
+
chainId: CHAIN_TO_CHAIN_ID[
|
|
5050
|
+
"ethereum"
|
|
5051
|
+
/* ETHEREUM */
|
|
5052
|
+
],
|
|
5053
|
+
bitcoinNetwork: CHAIN_TO_BITCOIN_NETWORK[
|
|
5054
|
+
"ethereum"
|
|
5055
|
+
/* ETHEREUM */
|
|
5056
|
+
],
|
|
5057
|
+
allowArbitraryBitcoinProvider: false,
|
|
5058
|
+
allowArbitraryEvmRpc: false,
|
|
5059
|
+
allowDevSinglePriceProvider: false,
|
|
5060
|
+
minBitcoinBalanceConfirmations: 6,
|
|
5061
|
+
minBitcoinSpendConfirmations: 6,
|
|
5062
|
+
minConsensusProviders: 2
|
|
5063
|
+
}
|
|
5064
|
+
};
|
|
5065
|
+
function getChainPolicy(chain) {
|
|
5066
|
+
const validated = validateChain(chain);
|
|
5067
|
+
const policy = POLICY_BY_CHAIN[validated];
|
|
5068
|
+
if (!policy) {
|
|
5069
|
+
throw new Error(`No chain policy registered for "${chain}"`);
|
|
5070
|
+
}
|
|
5071
|
+
return policy;
|
|
5072
|
+
}
|
|
4382
5073
|
async function executeVaultSnapshot(params) {
|
|
4383
5074
|
global.ethers = {
|
|
4384
|
-
...
|
|
5075
|
+
...import_ethers16.ethers,
|
|
4385
5076
|
providers: {
|
|
4386
|
-
StaticJsonRpcProvider:
|
|
5077
|
+
StaticJsonRpcProvider: import_ethers16.ethers.JsonRpcProvider
|
|
4387
5078
|
},
|
|
4388
|
-
Contract:
|
|
4389
|
-
utils:
|
|
5079
|
+
Contract: import_ethers16.ethers.Contract,
|
|
5080
|
+
utils: import_ethers16.ethers
|
|
4390
5081
|
// v6 moved utils to top level
|
|
4391
5082
|
};
|
|
4392
5083
|
global.Lit = {
|
|
@@ -4423,7 +5114,7 @@ ${auth.clientId}`;
|
|
|
4423
5114
|
// Default to mainnet if not provided
|
|
4424
5115
|
rpcUrl: params.rpcUrl,
|
|
4425
5116
|
bitcoinProvider,
|
|
4426
|
-
minConfirmations: params.minConfirmations
|
|
5117
|
+
minConfirmations: params.minConfirmations ?? getChainPolicy(params.chain).minBitcoinBalanceConfirmations
|
|
4427
5118
|
});
|
|
4428
5119
|
const vaultSnapshot = createVaultSnapshotModule({
|
|
4429
5120
|
contractAddress: params.contractAddress,
|
|
@@ -4657,7 +5348,8 @@ function getSepoliaConfig() {
|
|
|
4657
5348
|
contractVersionRegistry: SEPOLIA_CONTRACTS.ContractVersionRegistry || "",
|
|
4658
5349
|
feeRecipientRegistry: SEPOLIA_CONTRACTS.FeeRecipientRegistry || "",
|
|
4659
5350
|
positionDelegateRegistry: SEPOLIA_CONTRACTS.PositionDelegateRegistry || "",
|
|
4660
|
-
bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
|
|
5351
|
+
bitcoinWithdrawalAddressRegistry: SEPOLIA_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
|
|
5352
|
+
carryAgentRegistry: SEPOLIA_CONTRACTS.CarryAgentRegistry || ""
|
|
4661
5353
|
},
|
|
4662
5354
|
subgraphs: {
|
|
4663
5355
|
// Lazy getter for the same module-init snapshot reason as serviceEndpoint.
|
|
@@ -4697,7 +5389,8 @@ function getMainnetConfig() {
|
|
|
4697
5389
|
contractVersionRegistry: MAINNET_CONTRACTS.ContractVersionRegistry || "",
|
|
4698
5390
|
feeRecipientRegistry: MAINNET_CONTRACTS.FeeRecipientRegistry || "",
|
|
4699
5391
|
positionDelegateRegistry: MAINNET_CONTRACTS.PositionDelegateRegistry || "",
|
|
4700
|
-
bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || ""
|
|
5392
|
+
bitcoinWithdrawalAddressRegistry: MAINNET_CONTRACTS.BitcoinWithdrawalAddressRegistry || "",
|
|
5393
|
+
carryAgentRegistry: MAINNET_CONTRACTS.CarryAgentRegistry || ""
|
|
4701
5394
|
},
|
|
4702
5395
|
subgraphs: {
|
|
4703
5396
|
diamondHandsUrl: "https://gateway-arbitrum.network.thegraph.com/api/subgraphs/id/CCTPsdYqco2jChDLLBQTbdJWwoukVoMt1cXeR9ti6r9A"
|
|
@@ -4837,7 +5530,7 @@ var init_network_configs = __esm({
|
|
|
4837
5530
|
});
|
|
4838
5531
|
|
|
4839
5532
|
// src/modules/diamond-hands-sdk.ts
|
|
4840
|
-
import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder, keccak256 as keccak2565, solidityPackedKeccak256 as solidityPackedKeccak2564, toUtf8Bytes as toUtf8Bytes5, zeroPadValue as zeroPadValue4, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3, MaxUint256 } from "ethers";
|
|
5533
|
+
import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2565, solidityPackedKeccak256 as solidityPackedKeccak2564, toUtf8Bytes as toUtf8Bytes5, getBytes as getBytes4, zeroPadValue as zeroPadValue4, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3, MaxUint256 } from "ethers";
|
|
4841
5534
|
|
|
4842
5535
|
// src/types/result.ts
|
|
4843
5536
|
function success(value) {
|
|
@@ -5221,22 +5914,12 @@ var SDKError = class _SDKError extends Error {
|
|
|
5221
5914
|
|
|
5222
5915
|
// src/utils/chunks/eip1559-broadcast.utils.ts
|
|
5223
5916
|
import { parseUnits } from "ethers";
|
|
5224
|
-
var MINT_UCD_GAS_CEILING = BigInt(
|
|
5917
|
+
var MINT_UCD_GAS_CEILING = BigInt(13e5);
|
|
5225
5918
|
var WITHDRAW_BTC_GAS_CEILING = BigInt(3e6);
|
|
5226
5919
|
var MAKE_PAYMENT_GAS_CEILING = BigInt(1e6);
|
|
5227
5920
|
var EXTEND_POSITION_GAS_CEILING = BigInt(1e6);
|
|
5228
5921
|
var LIQUIDATION_COMMIT_GAS_CEILING = BigInt(1e6);
|
|
5229
5922
|
var LIQUIDATION_REVEAL_GAS_CEILING = BigInt(2e6);
|
|
5230
|
-
var MINT_UCD_ESTIMATE_MARGIN_BPS = 2500;
|
|
5231
|
-
async function estimateContractCallGasWithMargin(provider, from, to, data, marginBps, ceiling) {
|
|
5232
|
-
try {
|
|
5233
|
-
const estimated = await provider.estimateGas({ from, to, data });
|
|
5234
|
-
const withMargin = estimated * BigInt(1e4 + marginBps) / 10000n;
|
|
5235
|
-
return withMargin > ceiling ? ceiling : withMargin;
|
|
5236
|
-
} catch {
|
|
5237
|
-
return null;
|
|
5238
|
-
}
|
|
5239
|
-
}
|
|
5240
5923
|
async function resolveEip1559FeeFields(provider) {
|
|
5241
5924
|
const feeData = await provider.getFeeData();
|
|
5242
5925
|
let maxFeePerGas = feeData.maxFeePerGas ?? void 0;
|
|
@@ -5744,21 +6427,117 @@ async function buildSignedLoginPayload(signer, chainId, audience) {
|
|
|
5744
6427
|
return { chainId, message, signature };
|
|
5745
6428
|
}
|
|
5746
6429
|
|
|
6430
|
+
// src/utils/server-session-store.ts
|
|
6431
|
+
var REAUTH_WINDOW_SECONDS = 24 * 60 * 60;
|
|
6432
|
+
var SAFETY_MARGIN_SECONDS = 5 * 60;
|
|
6433
|
+
var STORE_PREFIX = "dh-server-session:";
|
|
6434
|
+
function sessionStoreKey(address, chainId, endpoint) {
|
|
6435
|
+
return STORE_PREFIX + `${address.trim().toLowerCase()}:${chainId}:${endpoint.replace(/\/+$/, "")}`;
|
|
6436
|
+
}
|
|
6437
|
+
function isPayloadShape(value) {
|
|
6438
|
+
return !!value && typeof value === "object" && typeof value.chainId === "number" && typeof value.signature === "string" && !!value.message && typeof value.message.address === "string" && typeof value.message.issuedAt === "number" && typeof value.message.nonce === "string";
|
|
6439
|
+
}
|
|
6440
|
+
function isPersistedShape(value) {
|
|
6441
|
+
return !!value && typeof value === "object" && value.version === 1 && typeof value.address === "string" && typeof value.chainId === "number" && typeof value.endpoint === "string" && isPayloadShape(value.payload) && (value.token === void 0 || typeof value.token === "string") && (value.tokenExpiresAt === void 0 || typeof value.tokenExpiresAt === "number");
|
|
6442
|
+
}
|
|
6443
|
+
var LocalStorageSessionStore = class {
|
|
6444
|
+
load(key) {
|
|
6445
|
+
let raw;
|
|
6446
|
+
try {
|
|
6447
|
+
raw = window.localStorage.getItem(key);
|
|
6448
|
+
} catch {
|
|
6449
|
+
return null;
|
|
6450
|
+
}
|
|
6451
|
+
if (!raw)
|
|
6452
|
+
return null;
|
|
6453
|
+
let parsed;
|
|
6454
|
+
try {
|
|
6455
|
+
parsed = JSON.parse(raw);
|
|
6456
|
+
} catch {
|
|
6457
|
+
this.clear(key);
|
|
6458
|
+
return null;
|
|
6459
|
+
}
|
|
6460
|
+
if (!isPersistedShape(parsed)) {
|
|
6461
|
+
this.clear(key);
|
|
6462
|
+
return null;
|
|
6463
|
+
}
|
|
6464
|
+
return parsed;
|
|
6465
|
+
}
|
|
6466
|
+
save(key, session) {
|
|
6467
|
+
try {
|
|
6468
|
+
window.localStorage.setItem(key, JSON.stringify(session));
|
|
6469
|
+
} catch {
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
clear(key) {
|
|
6473
|
+
try {
|
|
6474
|
+
window.localStorage.removeItem(key);
|
|
6475
|
+
} catch {
|
|
6476
|
+
}
|
|
6477
|
+
}
|
|
6478
|
+
};
|
|
6479
|
+
var MemorySessionStore = class {
|
|
6480
|
+
entries = /* @__PURE__ */ new Map();
|
|
6481
|
+
load(key) {
|
|
6482
|
+
return this.entries.get(key) ?? null;
|
|
6483
|
+
}
|
|
6484
|
+
save(key, session) {
|
|
6485
|
+
this.entries.set(key, session);
|
|
6486
|
+
}
|
|
6487
|
+
clear(key) {
|
|
6488
|
+
this.entries.delete(key);
|
|
6489
|
+
}
|
|
6490
|
+
};
|
|
6491
|
+
function localStorageUsable() {
|
|
6492
|
+
try {
|
|
6493
|
+
if (typeof window === "undefined" || !window.localStorage)
|
|
6494
|
+
return false;
|
|
6495
|
+
const probe = "__dh_server_session_probe__";
|
|
6496
|
+
window.localStorage.setItem(probe, "1");
|
|
6497
|
+
window.localStorage.removeItem(probe);
|
|
6498
|
+
return true;
|
|
6499
|
+
} catch {
|
|
6500
|
+
return false;
|
|
6501
|
+
}
|
|
6502
|
+
}
|
|
6503
|
+
function createDefaultSessionStore() {
|
|
6504
|
+
return localStorageUsable() ? new LocalStorageSessionStore() : new MemorySessionStore();
|
|
6505
|
+
}
|
|
6506
|
+
|
|
5747
6507
|
// src/utils/server-session.ts
|
|
5748
6508
|
var REFRESH_LEEWAY_SECONDS = 30;
|
|
6509
|
+
var ServerLoginError = class extends Error {
|
|
6510
|
+
constructor(message, status) {
|
|
6511
|
+
super(message);
|
|
6512
|
+
this.status = status;
|
|
6513
|
+
this.name = "ServerLoginError";
|
|
6514
|
+
}
|
|
6515
|
+
};
|
|
6516
|
+
function isLoginRejection(error) {
|
|
6517
|
+
return error instanceof ServerLoginError && error.status >= 400 && error.status < 500 && error.status !== 429;
|
|
6518
|
+
}
|
|
5749
6519
|
var ServerSession = class {
|
|
5750
6520
|
signer;
|
|
5751
6521
|
serviceEndpoint;
|
|
5752
6522
|
chainId;
|
|
6523
|
+
store;
|
|
6524
|
+
onSignaturePrompt;
|
|
5753
6525
|
now;
|
|
5754
6526
|
fetchImpl;
|
|
5755
6527
|
cached = null;
|
|
5756
6528
|
inFlight = null;
|
|
6529
|
+
/**
|
|
6530
|
+
* Store key for the current signer, memoized on first use so the sync
|
|
6531
|
+
* `clear()` can drop the persisted token without an async address lookup.
|
|
6532
|
+
*/
|
|
6533
|
+
lastStoreKey = null;
|
|
5757
6534
|
constructor(opts) {
|
|
5758
6535
|
assertSafeServiceEndpoint(opts.serviceEndpoint);
|
|
5759
6536
|
this.signer = opts.signer;
|
|
5760
6537
|
this.serviceEndpoint = opts.serviceEndpoint.replace(/\/+$/, "");
|
|
5761
6538
|
this.chainId = opts.chainId;
|
|
6539
|
+
this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
|
|
6540
|
+
this.onSignaturePrompt = opts.onSignaturePrompt;
|
|
5762
6541
|
this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
5763
6542
|
this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
|
|
5764
6543
|
}
|
|
@@ -5771,19 +6550,56 @@ var ServerSession = class {
|
|
|
5771
6550
|
async getAuthHeader() {
|
|
5772
6551
|
return { Authorization: `Bearer ${await this.getValidToken()}` };
|
|
5773
6552
|
}
|
|
5774
|
-
/**
|
|
6553
|
+
/**
|
|
6554
|
+
* Drop the cached token (e.g. on 401 from server). The persisted envelope is
|
|
6555
|
+
* kept — the next call re-mints silently — but its stored token copy is
|
|
6556
|
+
* dropped too so a dead JWT can't be re-adopted from the store.
|
|
6557
|
+
*/
|
|
5775
6558
|
clear() {
|
|
5776
6559
|
this.cached = null;
|
|
5777
6560
|
this.inFlight = null;
|
|
6561
|
+
if (!this.store || !this.lastStoreKey)
|
|
6562
|
+
return;
|
|
6563
|
+
const persisted = this.store.load(this.lastStoreKey);
|
|
6564
|
+
if (persisted?.token) {
|
|
6565
|
+
this.store.save(this.lastStoreKey, {
|
|
6566
|
+
...persisted,
|
|
6567
|
+
token: void 0,
|
|
6568
|
+
tokenExpiresAt: void 0
|
|
6569
|
+
});
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
/**
|
|
6573
|
+
* Drop the session AND the persisted re-mint envelope (wallet disconnect /
|
|
6574
|
+
* explicit sign-out paths — anything that must remove the 24h credential
|
|
6575
|
+
* from storage).
|
|
6576
|
+
*/
|
|
6577
|
+
async clearPersisted() {
|
|
6578
|
+
this.cached = null;
|
|
6579
|
+
this.inFlight = null;
|
|
6580
|
+
if (!this.store)
|
|
6581
|
+
return;
|
|
6582
|
+
if (this.lastStoreKey) {
|
|
6583
|
+
this.store.clear(this.lastStoreKey);
|
|
6584
|
+
return;
|
|
6585
|
+
}
|
|
6586
|
+
try {
|
|
6587
|
+
const address = (await this.signer.getAddress()).toLowerCase();
|
|
6588
|
+
this.store.clear(
|
|
6589
|
+
sessionStoreKey(address, this.chainId, this.serviceEndpoint)
|
|
6590
|
+
);
|
|
6591
|
+
} catch {
|
|
6592
|
+
}
|
|
5778
6593
|
}
|
|
5779
6594
|
/**
|
|
5780
|
-
* Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`)
|
|
5781
|
-
*
|
|
5782
|
-
*
|
|
6595
|
+
* Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`)
|
|
6596
|
+
* — which also tombstones the envelope's re-mint window server-side — and
|
|
6597
|
+
* remove the persisted credential locally. Clears the local cache regardless
|
|
6598
|
+
* of server response so the client stops presenting the token.
|
|
5783
6599
|
*/
|
|
5784
6600
|
async logout() {
|
|
5785
6601
|
const token = this.cached?.token ?? await this.getValidToken().catch(() => null);
|
|
5786
|
-
this.
|
|
6602
|
+
await this.clearPersisted();
|
|
5787
6603
|
if (!token)
|
|
5788
6604
|
return;
|
|
5789
6605
|
const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/logout`, {
|
|
@@ -5806,41 +6622,116 @@ var ServerSession = class {
|
|
|
5806
6622
|
return this.inFlight;
|
|
5807
6623
|
this.inFlight = (async () => {
|
|
5808
6624
|
try {
|
|
6625
|
+
const persisted = await this.loadPersisted();
|
|
6626
|
+
if (persisted) {
|
|
6627
|
+
if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
|
|
6628
|
+
this.cached = {
|
|
6629
|
+
token: persisted.token,
|
|
6630
|
+
expiresAt: persisted.tokenExpiresAt
|
|
6631
|
+
};
|
|
6632
|
+
return this.cached;
|
|
6633
|
+
}
|
|
6634
|
+
try {
|
|
6635
|
+
const session2 = await this.login(persisted.payload);
|
|
6636
|
+
this.persist(persisted.payload, session2);
|
|
6637
|
+
this.cached = session2;
|
|
6638
|
+
return session2;
|
|
6639
|
+
} catch (error) {
|
|
6640
|
+
if (!isLoginRejection(error))
|
|
6641
|
+
throw error;
|
|
6642
|
+
if (this.store && this.lastStoreKey) {
|
|
6643
|
+
this.store.clear(this.lastStoreKey);
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
this.onSignaturePrompt?.();
|
|
5809
6648
|
const payload = await buildSignedLoginPayload(
|
|
5810
6649
|
this.signer,
|
|
5811
6650
|
this.chainId,
|
|
5812
6651
|
this.serviceEndpoint
|
|
5813
6652
|
);
|
|
5814
|
-
const
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
headers: { "Content-Type": "application/json" },
|
|
5819
|
-
body: JSON.stringify(payload)
|
|
5820
|
-
}
|
|
5821
|
-
);
|
|
5822
|
-
if (!res.ok) {
|
|
5823
|
-
const detail = await res.text().catch(() => "");
|
|
5824
|
-
throw new Error(
|
|
5825
|
-
`lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`
|
|
5826
|
-
);
|
|
5827
|
-
}
|
|
5828
|
-
const body = await res.json();
|
|
5829
|
-
if (!body?.token || !body?.expiresAt) {
|
|
5830
|
-
throw new Error("lit-ops-server login returned malformed response");
|
|
5831
|
-
}
|
|
5832
|
-
this.cached = { token: body.token, expiresAt: body.expiresAt };
|
|
5833
|
-
return this.cached;
|
|
6653
|
+
const session = await this.login(payload);
|
|
6654
|
+
this.persist(payload, session);
|
|
6655
|
+
this.cached = session;
|
|
6656
|
+
return session;
|
|
5834
6657
|
} finally {
|
|
5835
6658
|
this.inFlight = null;
|
|
5836
6659
|
}
|
|
5837
6660
|
})();
|
|
5838
6661
|
return this.inFlight;
|
|
5839
6662
|
}
|
|
6663
|
+
async login(payload) {
|
|
6664
|
+
const res = await this.fetchImpl(`${this.serviceEndpoint}/api/auth/login`, {
|
|
6665
|
+
method: "POST",
|
|
6666
|
+
headers: { "Content-Type": "application/json" },
|
|
6667
|
+
body: JSON.stringify(payload)
|
|
6668
|
+
});
|
|
6669
|
+
if (!res.ok) {
|
|
6670
|
+
const detail = await res.text().catch(() => "");
|
|
6671
|
+
throw new ServerLoginError(
|
|
6672
|
+
`lit-ops-server login failed: HTTP ${res.status} ${detail.slice(0, 200)}`,
|
|
6673
|
+
res.status
|
|
6674
|
+
);
|
|
6675
|
+
}
|
|
6676
|
+
const body = await res.json();
|
|
6677
|
+
if (!body?.token || !body?.expiresAt) {
|
|
6678
|
+
throw new Error("lit-ops-server login returned malformed response");
|
|
6679
|
+
}
|
|
6680
|
+
return { token: body.token, expiresAt: body.expiresAt };
|
|
6681
|
+
}
|
|
6682
|
+
/**
|
|
6683
|
+
* Load the persisted session for the current signer, validating that it
|
|
6684
|
+
* belongs to this address/chain/endpoint and that the envelope is still
|
|
6685
|
+
* inside the re-auth window (with a safety margin). Invalid or expired
|
|
6686
|
+
* entries are evicted.
|
|
6687
|
+
*/
|
|
6688
|
+
async loadPersisted() {
|
|
6689
|
+
if (!this.store)
|
|
6690
|
+
return null;
|
|
6691
|
+
try {
|
|
6692
|
+
const address = (await this.signer.getAddress()).toLowerCase();
|
|
6693
|
+
const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
|
|
6694
|
+
this.lastStoreKey = key;
|
|
6695
|
+
const persisted = this.store.load(key);
|
|
6696
|
+
if (!persisted)
|
|
6697
|
+
return null;
|
|
6698
|
+
const withinWindow = this.now() < persisted.payload.message.issuedAt + REAUTH_WINDOW_SECONDS - SAFETY_MARGIN_SECONDS;
|
|
6699
|
+
if (persisted.address !== address || persisted.chainId !== this.chainId || persisted.endpoint !== this.serviceEndpoint || persisted.payload.message.address.toLowerCase() !== address || !withinWindow) {
|
|
6700
|
+
this.store.clear(key);
|
|
6701
|
+
return null;
|
|
6702
|
+
}
|
|
6703
|
+
return persisted;
|
|
6704
|
+
} catch {
|
|
6705
|
+
return null;
|
|
6706
|
+
}
|
|
6707
|
+
}
|
|
6708
|
+
/** Best-effort save of the envelope + live JWT for silent recovery. */
|
|
6709
|
+
persist(payload, session) {
|
|
6710
|
+
if (!this.store)
|
|
6711
|
+
return;
|
|
6712
|
+
const address = payload.message.address.toLowerCase();
|
|
6713
|
+
try {
|
|
6714
|
+
const key = sessionStoreKey(address, this.chainId, this.serviceEndpoint);
|
|
6715
|
+
this.lastStoreKey = key;
|
|
6716
|
+
this.store.save(key, {
|
|
6717
|
+
version: 1,
|
|
6718
|
+
address,
|
|
6719
|
+
chainId: this.chainId,
|
|
6720
|
+
endpoint: this.serviceEndpoint,
|
|
6721
|
+
payload,
|
|
6722
|
+
token: session.token,
|
|
6723
|
+
tokenExpiresAt: session.expiresAt
|
|
6724
|
+
});
|
|
6725
|
+
} catch {
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
5840
6728
|
};
|
|
5841
6729
|
|
|
5842
6730
|
// src/utils/quantum-revert.utils.ts
|
|
6731
|
+
import { AbiCoder } from "ethers";
|
|
5843
6732
|
var DEAD_ZONE_VIOLATION_SELECTOR = "0xbe4b82c1";
|
|
6733
|
+
var ERROR_STRING_SELECTOR = "0x08c379a0";
|
|
6734
|
+
var PANIC_SELECTOR = "0x4e487b71";
|
|
5844
6735
|
var QUANTUM_REVERT_NAMES = {
|
|
5845
6736
|
"0xbe4b82c1": "DeadZoneViolation()",
|
|
5846
6737
|
"0x137f3b70": "InDeadZone()",
|
|
@@ -5864,14 +6755,128 @@ function decodeQuantumRevert(e) {
|
|
|
5864
6755
|
e?.error?.data?.data ?? e?.error?.data ?? e?.error?.error?.data;
|
|
5865
6756
|
const data = typeof raw === "string" && raw.startsWith("0x") ? raw : null;
|
|
5866
6757
|
const selector = data && data.length >= 10 ? data.slice(0, 10) : null;
|
|
5867
|
-
|
|
6758
|
+
let name = selector ? QUANTUM_REVERT_NAMES[selector] ?? null : null;
|
|
6759
|
+
if (!name && selector && data) {
|
|
6760
|
+
try {
|
|
6761
|
+
if (selector === ERROR_STRING_SELECTOR) {
|
|
6762
|
+
const [message] = AbiCoder.defaultAbiCoder().decode(
|
|
6763
|
+
["string"],
|
|
6764
|
+
"0x" + data.slice(10)
|
|
6765
|
+
);
|
|
6766
|
+
name = `Error("${message}")`;
|
|
6767
|
+
} else if (selector === PANIC_SELECTOR) {
|
|
6768
|
+
const [code] = AbiCoder.defaultAbiCoder().decode(
|
|
6769
|
+
["uint256"],
|
|
6770
|
+
"0x" + data.slice(10)
|
|
6771
|
+
);
|
|
6772
|
+
name = `Panic(0x${code.toString(16)})`;
|
|
6773
|
+
}
|
|
6774
|
+
} catch {
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
5868
6777
|
return { data, selector, name };
|
|
5869
6778
|
}
|
|
5870
6779
|
|
|
6780
|
+
// src/utils/quantum-submission.utils.ts
|
|
6781
|
+
var MAX_DEADZONE_RESIMULATIONS = 3;
|
|
6782
|
+
var QuantumRevertError = class extends Error {
|
|
6783
|
+
constructor(errorName, selector, data, cause) {
|
|
6784
|
+
super(
|
|
6785
|
+
`Pre-send simulation reverted: ${errorName ?? selector ?? "unknown"}`
|
|
6786
|
+
);
|
|
6787
|
+
this.errorName = errorName;
|
|
6788
|
+
this.selector = selector;
|
|
6789
|
+
this.data = data;
|
|
6790
|
+
this.cause = cause;
|
|
6791
|
+
this.name = "QuantumRevertError";
|
|
6792
|
+
}
|
|
6793
|
+
};
|
|
6794
|
+
async function assertSafeQuantumSubmission(opts) {
|
|
6795
|
+
const { provider, to, from, data, quantumTimestamp } = opts;
|
|
6796
|
+
const maxResimulations = opts.maxResimulations ?? MAX_DEADZONE_RESIMULATIONS;
|
|
6797
|
+
const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
6798
|
+
const onDebug = opts.onDebug ?? (() => {
|
|
6799
|
+
});
|
|
6800
|
+
const gateOpts = {
|
|
6801
|
+
now,
|
|
6802
|
+
...opts.sleep ? { sleep: opts.sleep } : {}
|
|
6803
|
+
};
|
|
6804
|
+
await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
|
|
6805
|
+
for (let sim = 1; ; sim++) {
|
|
6806
|
+
try {
|
|
6807
|
+
await provider.call({ to, from, data });
|
|
6808
|
+
break;
|
|
6809
|
+
} catch (simError) {
|
|
6810
|
+
const decoded = decodeQuantumRevert(simError);
|
|
6811
|
+
if (decoded.selector === DEAD_ZONE_VIOLATION_SELECTOR && sim <= maxResimulations) {
|
|
6812
|
+
const sigQuantum = Math.floor(quantumTimestamp / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
|
|
6813
|
+
const realCurrentQuantum = Math.floor(now() / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
|
|
6814
|
+
if (sigQuantum === realCurrentQuantum) {
|
|
6815
|
+
onDebug(
|
|
6816
|
+
"\u21AA\uFE0F Simulated DeadZoneViolation is a stale-latest-block artifact (signature is the current quantum) \u2014 proceeding to broadcast"
|
|
6817
|
+
);
|
|
6818
|
+
break;
|
|
6819
|
+
}
|
|
6820
|
+
const gate = await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
|
|
6821
|
+
if (gate.waited) {
|
|
6822
|
+
onDebug(
|
|
6823
|
+
`\u{1F501} Re-simulating after crossing the quantum boundary (attempt ${sim}/${maxResimulations})`
|
|
6824
|
+
);
|
|
6825
|
+
continue;
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
if (!decoded.data) {
|
|
6829
|
+
const label = simError instanceof Error ? simError.message : String(simError);
|
|
6830
|
+
onDebug(
|
|
6831
|
+
`\u26A0\uFE0F Pre-send simulation failed without revert data (${label}) \u2014 proceeding to broadcast`
|
|
6832
|
+
);
|
|
6833
|
+
break;
|
|
6834
|
+
}
|
|
6835
|
+
throw new QuantumRevertError(
|
|
6836
|
+
decoded.name,
|
|
6837
|
+
decoded.selector,
|
|
6838
|
+
decoded.data,
|
|
6839
|
+
simError
|
|
6840
|
+
);
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
|
|
6844
|
+
}
|
|
6845
|
+
|
|
5871
6846
|
// src/utils/btc-withdrawal-message.ts
|
|
5872
6847
|
import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
|
|
5873
6848
|
var QUANTUM_SECONDS = 60;
|
|
5874
6849
|
var QUANTUM_SAFE_THRESHOLD = 40;
|
|
6850
|
+
function hashBtcInputSet(utxos) {
|
|
6851
|
+
if (!Array.isArray(utxos) || utxos.length === 0) {
|
|
6852
|
+
throw new Error("hashBtcInputSet: at least one input is required");
|
|
6853
|
+
}
|
|
6854
|
+
const normalized = utxos.map((u) => {
|
|
6855
|
+
const txid = String(u.txid).toLowerCase().replace(/^0x/, "");
|
|
6856
|
+
const vout = Number(u.vout);
|
|
6857
|
+
const value = Number(u.value);
|
|
6858
|
+
if (!/^[0-9a-f]{64}$/.test(txid)) {
|
|
6859
|
+
throw new Error(`hashBtcInputSet: invalid txid "${u.txid}"`);
|
|
6860
|
+
}
|
|
6861
|
+
if (!Number.isSafeInteger(vout) || vout < 0) {
|
|
6862
|
+
throw new Error(`hashBtcInputSet: invalid vout ${u.vout} for ${txid}`);
|
|
6863
|
+
}
|
|
6864
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
6865
|
+
throw new Error(`hashBtcInputSet: invalid value ${u.value} for ${txid}:${vout}`);
|
|
6866
|
+
}
|
|
6867
|
+
return { txid, vout, value };
|
|
6868
|
+
});
|
|
6869
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6870
|
+
for (const u of normalized) {
|
|
6871
|
+
const key = `${u.txid}:${u.vout}`;
|
|
6872
|
+
if (seen.has(key)) {
|
|
6873
|
+
throw new Error(`hashBtcInputSet: duplicate outpoint ${key}`);
|
|
6874
|
+
}
|
|
6875
|
+
seen.add(key);
|
|
6876
|
+
}
|
|
6877
|
+
const canonical = normalized.sort((a, b) => a.txid < b.txid ? -1 : a.txid > b.txid ? 1 : a.vout - b.vout).map((u) => `${u.txid}:${u.vout}:${u.value}`).join("|");
|
|
6878
|
+
return keccak2563(toUtf8Bytes3(canonical));
|
|
6879
|
+
}
|
|
5875
6880
|
async function buildBtcExecuteEnvelope(params) {
|
|
5876
6881
|
const {
|
|
5877
6882
|
positionId,
|
|
@@ -5892,6 +6897,17 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
5892
6897
|
"buildBtcExecuteEnvelope: targetAmount must be a positive number of satoshis"
|
|
5893
6898
|
);
|
|
5894
6899
|
}
|
|
6900
|
+
const utxos = params.utxos && params.utxos.length > 0 ? params.utxos : [{ txid, vout, value: satoshis }];
|
|
6901
|
+
const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
|
|
6902
|
+
const authorizedInSet = utxos.some(
|
|
6903
|
+
(u) => normTxid(u.txid) === normTxid(txid) && Number(u.vout) === Number(vout)
|
|
6904
|
+
);
|
|
6905
|
+
if (!authorizedInSet) {
|
|
6906
|
+
throw new Error(
|
|
6907
|
+
`buildBtcExecuteEnvelope: the authorized UTXO ${txid}:${vout} must be part of utxos[]`
|
|
6908
|
+
);
|
|
6909
|
+
}
|
|
6910
|
+
const utxosHash = hashBtcInputSet(utxos);
|
|
5895
6911
|
const nowSec = Date.now() / 1e3;
|
|
5896
6912
|
const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
|
|
5897
6913
|
const secsIntoQuantum = nowSec - currentQuantumStart;
|
|
@@ -5928,6 +6944,8 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
5928
6944
|
// targetAddress (BTC destination)
|
|
5929
6945
|
"uint256",
|
|
5930
6946
|
// targetAmount (sats authorized to recipient)
|
|
6947
|
+
"bytes32",
|
|
6948
|
+
// utxosHash (canonical hash of the FULL input set — P6/#8)
|
|
5931
6949
|
"bytes32"
|
|
5932
6950
|
// actionHash
|
|
5933
6951
|
],
|
|
@@ -5939,6 +6957,7 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
5939
6957
|
networkFee,
|
|
5940
6958
|
targetAddress,
|
|
5941
6959
|
targetAmount,
|
|
6960
|
+
utxosHash,
|
|
5942
6961
|
actionHash
|
|
5943
6962
|
]
|
|
5944
6963
|
);
|
|
@@ -5955,7 +6974,8 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
5955
6974
|
chainId,
|
|
5956
6975
|
timestamp,
|
|
5957
6976
|
userSignature,
|
|
5958
|
-
borrowerAddress
|
|
6977
|
+
borrowerAddress,
|
|
6978
|
+
utxos
|
|
5959
6979
|
};
|
|
5960
6980
|
}
|
|
5961
6981
|
|
|
@@ -8771,6 +9791,56 @@ var LoanQuery = class {
|
|
|
8771
9791
|
})
|
|
8772
9792
|
);
|
|
8773
9793
|
}
|
|
9794
|
+
/**
|
|
9795
|
+
* Get loan-event history for every position owned by a wallet.
|
|
9796
|
+
*
|
|
9797
|
+
* Composes getUserPositions + getLoanEvents (the subgraph has no wallet-scoped
|
|
9798
|
+
* event collection). Each returned LoanEvents carries its own positionId, so
|
|
9799
|
+
* callers can flatten and merge into a single per-wallet timeline.
|
|
9800
|
+
*
|
|
9801
|
+
* Bitcoin deposits never appear in the subgraph — merge those in from an
|
|
9802
|
+
* Esplora source on the consumer side.
|
|
9803
|
+
*
|
|
9804
|
+
* @param userAddress - Loan-owner wallet address.
|
|
9805
|
+
* @param filter - Optional per-position event filter (same semantics as getLoanEvents).
|
|
9806
|
+
* @returns One LoanEvents object per position (empty array if the wallet has none).
|
|
9807
|
+
*
|
|
9808
|
+
* @example
|
|
9809
|
+
* ```typescript
|
|
9810
|
+
* const result = await loanQuery.getWalletLoanEvents('0x830b...');
|
|
9811
|
+
* if (result.success) {
|
|
9812
|
+
* const allEvents = result.value; // LoanEvents[] — one per position
|
|
9813
|
+
* }
|
|
9814
|
+
* ```
|
|
9815
|
+
*/
|
|
9816
|
+
async getWalletLoanEvents(userAddress, filter) {
|
|
9817
|
+
if (this.config.debug) {
|
|
9818
|
+
log.info("\u{1F50D} Querying wallet loan events", { userAddress, filter });
|
|
9819
|
+
}
|
|
9820
|
+
return tryCatchAsync(
|
|
9821
|
+
async () => {
|
|
9822
|
+
const perPosition = await this.config.graphClient.getWalletLoanEvents(
|
|
9823
|
+
userAddress,
|
|
9824
|
+
filter
|
|
9825
|
+
);
|
|
9826
|
+
if (this.config.debug) {
|
|
9827
|
+
log.info("\u2705 Retrieved wallet loan events", {
|
|
9828
|
+
userAddress,
|
|
9829
|
+
positions: perPosition.length,
|
|
9830
|
+
totalEvents: perPosition.reduce((n, p) => n + p.totalEvents, 0)
|
|
9831
|
+
});
|
|
9832
|
+
}
|
|
9833
|
+
return perPosition;
|
|
9834
|
+
},
|
|
9835
|
+
(error) => new SDKError({
|
|
9836
|
+
message: "Failed to query wallet loan events",
|
|
9837
|
+
category: "SUBGRAPH" /* SUBGRAPH */,
|
|
9838
|
+
severity: "MEDIUM" /* MEDIUM */,
|
|
9839
|
+
originalError: error instanceof Error ? error : new Error(String(error)),
|
|
9840
|
+
context: { userAddress, filter }
|
|
9841
|
+
})
|
|
9842
|
+
);
|
|
9843
|
+
}
|
|
8774
9844
|
/**
|
|
8775
9845
|
* Clear loan cache
|
|
8776
9846
|
*
|
|
@@ -11512,6 +12582,39 @@ var DiamondHandsGraph = class {
|
|
|
11512
12582
|
totalEvents
|
|
11513
12583
|
};
|
|
11514
12584
|
}
|
|
12585
|
+
/**
|
|
12586
|
+
* Get the full loan-event history for every position owned by a wallet.
|
|
12587
|
+
*
|
|
12588
|
+
* The subgraph has no wallet-scoped event collection (`User` only derives
|
|
12589
|
+
* `positions`), so this composes the two existing wallet/position queries:
|
|
12590
|
+
* `getUserPositions(wallet)` to enumerate the wallet's positions, then
|
|
12591
|
+
* `getLoanEvents(positionId, filter)` per position. Each returned `LoanEvents`
|
|
12592
|
+
* already carries its own `positionId`, so callers can flatten and merge into
|
|
12593
|
+
* a single timeline.
|
|
12594
|
+
*
|
|
12595
|
+
* Note: Bitcoin deposits are UTXOs on the Bitcoin network and never appear in
|
|
12596
|
+
* the EVM subgraph — surface those from an Esplora source and merge separately.
|
|
12597
|
+
*
|
|
12598
|
+
* @param userAddress - The loan-owner wallet address.
|
|
12599
|
+
* @param filter - Optional per-position event filter (same semantics as getLoanEvents).
|
|
12600
|
+
* @returns One LoanEvents object per position; empty array if the wallet has none.
|
|
12601
|
+
*/
|
|
12602
|
+
async getWalletLoanEvents(userAddress, filter) {
|
|
12603
|
+
const { positions } = await this.getUserPositions(userAddress);
|
|
12604
|
+
const positionIds = positions.map((p) => p?.id != null ? String(p.id) : "").filter((id) => id.length > 0);
|
|
12605
|
+
if (positionIds.length === 0)
|
|
12606
|
+
return [];
|
|
12607
|
+
const CONCURRENCY = 5;
|
|
12608
|
+
const results = [];
|
|
12609
|
+
for (let i = 0; i < positionIds.length; i += CONCURRENCY) {
|
|
12610
|
+
const batch = positionIds.slice(i, i + CONCURRENCY);
|
|
12611
|
+
const settled = await Promise.all(
|
|
12612
|
+
batch.map((positionId) => this.getLoanEvents(positionId, filter))
|
|
12613
|
+
);
|
|
12614
|
+
results.push(...settled);
|
|
12615
|
+
}
|
|
12616
|
+
return results;
|
|
12617
|
+
}
|
|
11515
12618
|
// ============================================================================
|
|
11516
12619
|
// Protocol-wide Events
|
|
11517
12620
|
// ============================================================================
|
|
@@ -14928,6 +16031,15 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
14928
16031
|
return {};
|
|
14929
16032
|
return this.serverSession.getAuthHeader();
|
|
14930
16033
|
}
|
|
16034
|
+
/**
|
|
16035
|
+
* Remove the persisted lit-ops-server session (the 24h re-mint envelope and
|
|
16036
|
+
* its JWT) for the current auth signer. Call on wallet disconnect or
|
|
16037
|
+
* explicit sign-out so the credential does not outlive the wallet
|
|
16038
|
+
* connection. No-op in standalone mode.
|
|
16039
|
+
*/
|
|
16040
|
+
async clearServerSession() {
|
|
16041
|
+
await this.serverSession?.clearPersisted();
|
|
16042
|
+
}
|
|
14931
16043
|
/**
|
|
14932
16044
|
* Audit H-9: invalidate the LoanQuery cache so subsequent reads return
|
|
14933
16045
|
* the post-write state. We clear the entire loan-query cache (not just
|
|
@@ -15039,7 +16151,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
15039
16151
|
this.serverSession = new ServerSession({
|
|
15040
16152
|
signer: config.authSigner ?? config.contractSigner,
|
|
15041
16153
|
serviceEndpoint: config.serviceEndpoint,
|
|
15042
|
-
chainId: config.chainId
|
|
16154
|
+
chainId: config.chainId,
|
|
16155
|
+
persistSession: config.sessionPersistence?.enabled,
|
|
16156
|
+
sessionStore: config.sessionPersistence?.store,
|
|
16157
|
+
onSignaturePrompt: config.onSessionSignaturePrompt
|
|
15043
16158
|
});
|
|
15044
16159
|
}
|
|
15045
16160
|
const contractManagerResult = createContractManager({
|
|
@@ -16223,8 +17338,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
16223
17338
|
throw new Error(`Position not found: ${request.positionId}`);
|
|
16224
17339
|
}
|
|
16225
17340
|
const currentDebt = position.ucdDebt.toString();
|
|
16226
|
-
const ucdDebtHash = keccak2565(
|
|
16227
|
-
const contractHash = keccak2565(
|
|
17341
|
+
const ucdDebtHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
|
|
17342
|
+
const contractHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(
|
|
16228
17343
|
["address", "address", "address", "address"],
|
|
16229
17344
|
[
|
|
16230
17345
|
positionManagerAddress,
|
|
@@ -16235,263 +17350,55 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
16235
17350
|
));
|
|
16236
17351
|
if (this.config.debug) {
|
|
16237
17352
|
log.info("\u{1F50D} DEBUG: Detailed Contract Call Parameters", {});
|
|
16238
|
-
log.info(" === LIT Action Response ===", {});
|
|
16239
|
-
log.info(
|
|
16240
|
-
` authorizedSpendsHash: ${validationResponse.authorizedSpendsHash}`,
|
|
16241
|
-
{}
|
|
16242
|
-
);
|
|
16243
|
-
log.info(` mintAmount: ${validationResponse.mintAmount}`, {});
|
|
16244
|
-
log.info(` mintFee: ${validationResponse.mintFee}`, {});
|
|
16245
|
-
log.info(` newDebt: ${validationResponse.newDebt}`, {});
|
|
16246
|
-
log.info(` newCollateral: ${validationResponse.newCollateral}`, {});
|
|
16247
|
-
log.info(` btcPrice: ${validationResponse.btcPrice}`, {});
|
|
16248
|
-
log.info(
|
|
16249
|
-
` timestamp (quantumTimestamp): ${validationResponse.timestamp}`,
|
|
16250
|
-
{}
|
|
16251
|
-
);
|
|
16252
|
-
log.info(" === SDK Calculated Hashes ===", {});
|
|
16253
|
-
log.info(` Current UCD Debt (from position): ${currentDebt}`, {});
|
|
16254
|
-
log.info(` ucdDebtHash: ${ucdDebtHash}`, {});
|
|
16255
|
-
log.info(" === Contract Addresses for contractHash ===", {});
|
|
16256
|
-
log.info(` PositionManager: ${positionManagerAddress}`, {});
|
|
16257
|
-
log.info(
|
|
16258
|
-
` LoanOpsManager: ${this.getContractAddressesOrThrow().loanOperationsManager}`,
|
|
16259
|
-
{}
|
|
16260
|
-
);
|
|
16261
|
-
log.info(
|
|
16262
|
-
` TermManager: ${this.getContractAddressesOrThrow().termManager}`,
|
|
16263
|
-
{}
|
|
16264
|
-
);
|
|
16265
|
-
log.info(
|
|
16266
|
-
` UCDController: ${this.getContractAddressesOrThrow().ucdController}`,
|
|
16267
|
-
{}
|
|
16268
|
-
);
|
|
16269
|
-
log.info(` contractHash: ${contractHash}`, {});
|
|
16270
|
-
log.info(" === Signature ===", {});
|
|
16271
|
-
log.info(
|
|
16272
|
-
` Signature (first 20 chars): ${finalSignature.substring(
|
|
16273
|
-
0,
|
|
16274
|
-
20
|
|
16275
|
-
)}...`,
|
|
16276
|
-
{}
|
|
16277
|
-
);
|
|
16278
|
-
log.info(` Signature length: ${finalSignature.length}`, {});
|
|
16279
|
-
log.info(
|
|
16280
|
-
` Final signature (first 50 chars): ${finalSignature ? finalSignature.substring(0, 50) : "EMPTY"}`,
|
|
16281
|
-
{}
|
|
16282
|
-
);
|
|
16283
|
-
log.info(
|
|
16284
|
-
` Final signature is empty: ${!finalSignature || finalSignature.length === 0}`,
|
|
16285
|
-
{}
|
|
16286
|
-
);
|
|
16287
|
-
}
|
|
16288
|
-
if (this.config.debug) {
|
|
16289
|
-
log.info("\u{1F50D} PREFLIGHT: Attempting diagnostic gas estimation...", {});
|
|
16290
|
-
}
|
|
16291
|
-
try {
|
|
16292
|
-
const iface = new Interface4(positionManagerAbi);
|
|
16293
|
-
const signatureHex = finalSignature.startsWith("0x") ? finalSignature : "0x" + finalSignature;
|
|
16294
|
-
if (this.config.debug) {
|
|
16295
|
-
log.info("\u{1F50D} DEBUG: Signature details:", {
|
|
16296
|
-
finalSignature: finalSignature ? `${finalSignature.substring(0, 20)}... (length: ${finalSignature.length})` : "UNDEFINED/EMPTY",
|
|
16297
|
-
signatureHex: signatureHex ? `${signatureHex.substring(0, 22)}... (length: ${signatureHex.length})` : "UNDEFINED/EMPTY"
|
|
16298
|
-
});
|
|
16299
|
-
}
|
|
16300
|
-
const calldata = iface.encodeFunctionData("mintUCD", [
|
|
16301
|
-
positionIdBytes32,
|
|
16302
|
-
validationResponse.mintAmount,
|
|
16303
|
-
validationResponse.mintFee,
|
|
16304
|
-
validationResponse.newDebt,
|
|
16305
|
-
validationResponse.newCollateral,
|
|
16306
|
-
validationResponse.btcPrice,
|
|
16307
|
-
validationResponse.authorizedSpendsHash,
|
|
16308
|
-
ucdDebtHash,
|
|
16309
|
-
contractHash,
|
|
16310
|
-
validationResponse.timestamp,
|
|
16311
|
-
signatureHex
|
|
16312
|
-
]);
|
|
16313
|
-
if (this.config.debug) {
|
|
16314
|
-
log.info("\u{1F50D} DEBUG: Encoded calldata:", {
|
|
16315
|
-
length: calldata.length,
|
|
16316
|
-
signaturePortion: calldata.substring(650, 670)
|
|
16317
|
-
});
|
|
16318
|
-
}
|
|
16319
|
-
const provider = this.getSignerOrThrow().provider;
|
|
16320
|
-
if (!provider) {
|
|
16321
|
-
throw new Error("Provider not available");
|
|
16322
|
-
}
|
|
16323
|
-
const signerAddress = await this.getSignerOrThrow().getAddress();
|
|
16324
|
-
if (this.config.debug) {
|
|
16325
|
-
log.info(" Executing diagnostic eth_call...", {});
|
|
16326
|
-
}
|
|
16327
|
-
await provider.call({
|
|
16328
|
-
to: positionManagerAddress,
|
|
16329
|
-
from: signerAddress,
|
|
16330
|
-
data: calldata
|
|
16331
|
-
});
|
|
16332
|
-
if (this.config.debug) {
|
|
16333
|
-
log.info(" \u2705 Diagnostic eth_call succeeded!", {});
|
|
16334
|
-
}
|
|
16335
|
-
} catch (preflightError) {
|
|
16336
|
-
if (this.config.debug) {
|
|
16337
|
-
log.error(
|
|
16338
|
-
"\u274C PREFLIGHT CHECK FAILED - Diagnostic eth_call reverted",
|
|
16339
|
-
{}
|
|
16340
|
-
);
|
|
16341
|
-
log.error(` Error message: ${preflightError?.message}`, {});
|
|
16342
|
-
log.error(` Error code: ${preflightError?.code}`, {});
|
|
16343
|
-
log.error(` Error data: ${preflightError?.data || "none"}`, {});
|
|
16344
|
-
log.error(` Error reason: ${preflightError?.reason || "none"}`, {});
|
|
16345
|
-
log.error(
|
|
16346
|
-
` Error.error: ${JSON.stringify(preflightError?.error || {})}`,
|
|
16347
|
-
{}
|
|
16348
|
-
);
|
|
16349
|
-
}
|
|
16350
|
-
let errorData = preflightError?.data || preflightError?.info?.error?.data || preflightError?.error?.data?.data || preflightError?.error?.data || preflightError?.error?.error?.data;
|
|
16351
|
-
const errorMessage = preflightError?.reason || preflightError?.error?.reason || preflightError?.error?.error?.reason || preflightError?.message || "Unknown error";
|
|
16352
|
-
let requireMessage;
|
|
16353
|
-
if (errorData && typeof errorData === "string" && errorData.startsWith("0x") && errorData.length > 138) {
|
|
16354
|
-
try {
|
|
16355
|
-
const errorSelector = errorData.slice(0, 10);
|
|
16356
|
-
if (errorSelector === "0x08c379a0") {
|
|
16357
|
-
const iface = new Interface4([
|
|
16358
|
-
"function Error(string)"
|
|
16359
|
-
]);
|
|
16360
|
-
const decoded = iface.decodeFunctionData("Error", errorData);
|
|
16361
|
-
requireMessage = decoded[0];
|
|
16362
|
-
}
|
|
16363
|
-
} catch (e) {
|
|
16364
|
-
}
|
|
16365
|
-
}
|
|
16366
|
-
const isDeadZonePreflightArtifact = decodeQuantumRevert(preflightError).selector === DEAD_ZONE_VIOLATION_SELECTOR;
|
|
16367
|
-
if (isDeadZonePreflightArtifact) {
|
|
16368
|
-
const nowSec = Math.floor(Date.now() / 1e3);
|
|
16369
|
-
const sigQuantum2 = Math.floor(Number(validationResponse.timestamp) / 60) * 60;
|
|
16370
|
-
const realCurrentQuantum = Math.floor(nowSec / 60) * 60;
|
|
16371
|
-
if (sigQuantum2 === realCurrentQuantum) {
|
|
16372
|
-
if (this.config.debug) {
|
|
16373
|
-
log.info(
|
|
16374
|
-
"\u21AA\uFE0F Preflight DeadZoneViolation is a stale-latest-block artifact (signature is the current quantum) \u2014 proceeding to broadcast",
|
|
16375
|
-
{}
|
|
16376
|
-
);
|
|
16377
|
-
}
|
|
16378
|
-
} else {
|
|
16379
|
-
if (this.config.debug) {
|
|
16380
|
-
log.info(
|
|
16381
|
-
"\u{1F501} Preflight DeadZoneViolation near the boundary \u2014 deferring across it, then proceeding",
|
|
16382
|
-
{}
|
|
16383
|
-
);
|
|
16384
|
-
}
|
|
16385
|
-
await awaitSafeSubmissionWindow(
|
|
16386
|
-
Number(validationResponse.timestamp)
|
|
16387
|
-
);
|
|
16388
|
-
}
|
|
16389
|
-
}
|
|
16390
|
-
if (requireMessage && !isDeadZonePreflightArtifact) {
|
|
16391
|
-
throw new Error(
|
|
16392
|
-
`Preflight check failed - mintUCD would revert:
|
|
16393
|
-
${requireMessage}
|
|
16394
|
-
Context: Quantum timestamp=${validationResponse.timestamp}, Position=${request.positionId}`
|
|
16395
|
-
);
|
|
16396
|
-
}
|
|
16397
|
-
if (errorData && typeof errorData === "string" && errorData.startsWith("0x") && errorData.length > 10) {
|
|
16398
|
-
const selector = errorData.slice(0, 10);
|
|
16399
|
-
const knownErrors = {
|
|
16400
|
-
"0x8baa579f": "InvalidSignature()",
|
|
16401
|
-
"0xd92e233d": "ValidationFailed()",
|
|
16402
|
-
"0x3ee5aeb5": "OperationNotAuthorized()",
|
|
16403
|
-
"0x70f4b32a": "UnauthorizedSigner()",
|
|
16404
|
-
"0x48f5c3ed": "Unauthorized()",
|
|
16405
|
-
"0x36bfdeea": "StateHashMismatch()",
|
|
16406
|
-
"0x131d9a21": "QuantumExpired()",
|
|
16407
|
-
"0x52ce5d58": "QuantumAlreadyUsed()",
|
|
16408
|
-
"0x137f3b70": "InDeadZone()",
|
|
16409
|
-
"0xbe4b82c1": "DeadZoneViolation()",
|
|
16410
|
-
"0x3e76a3c9": "QuantumOutsideWindow()",
|
|
16411
|
-
"0x62278171": "InvalidValidatorSignature()",
|
|
16412
|
-
"0xb9d419a7": "DebtUpdateVerificationFailed()",
|
|
16413
|
-
"0xc7e20553": "DebtUpdateVerificationFailedDetailed(bytes32,uint256,uint256,uint256,uint256,uint256)",
|
|
16414
|
-
"0x0cfd2a97": "MintVerificationMismatch(uint256,uint256)",
|
|
16415
|
-
"0x07637bd8": "MintFailed()"
|
|
16416
|
-
};
|
|
16417
|
-
const errorName = knownErrors[selector] || `Unknown error ${selector}`;
|
|
16418
|
-
const mintDebtDiagnostics = selector === "0xb9d419a7" || selector === "0xc7e20553" || selector === "0x0cfd2a97" || selector === "0x07637bd8" ? `
|
|
16419
|
-
|
|
16420
|
-
Mint/debt diagnostics (LoanOperationsManager mint path \u2192 increaseDebtFromMint):
|
|
16421
|
-
positionId: ${request.positionId}
|
|
16422
|
-
on-chain ucdDebt before tx: ${currentDebt}
|
|
16423
|
-
Lit newDebt: ${validationResponse.newDebt}
|
|
16424
|
-
Lit mintAmount: ${validationResponse.mintAmount}
|
|
16425
|
-
Lit mintFee: ${validationResponse.mintFee}
|
|
16426
|
-
Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
|
|
16427
|
-
Contract expects UCD supply increase to match mintAmount+mintFee and debt update to newDebt.
|
|
16428
|
-
` : "";
|
|
16429
|
-
let quantumContext = "";
|
|
16430
|
-
if (selector === "0x131d9a21" || selector === "0x52ce5d58" || selector === "0x137f3b70" || selector === "0xbe4b82c1" || selector === "0x3e76a3c9") {
|
|
16431
|
-
const currentTime2 = Math.floor(Date.now() / 1e3);
|
|
16432
|
-
const currentQuantum = Math.floor(currentTime2 / 60) * 60;
|
|
16433
|
-
const sigQuantum2 = Math.floor(validationResponse.timestamp / 60) * 60;
|
|
16434
|
-
const timeDiff = currentTime2 - validationResponse.timestamp;
|
|
16435
|
-
quantumContext = `
|
|
16436
|
-
|
|
16437
|
-
Quantum Timing Analysis:
|
|
16438
|
-
Signature created: ${validationResponse.timestamp} (${new Date(
|
|
16439
|
-
validationResponse.timestamp * 1e3
|
|
16440
|
-
).toISOString()})
|
|
16441
|
-
Current time: ${currentTime2} (${new Date(
|
|
16442
|
-
currentTime2 * 1e3
|
|
16443
|
-
).toISOString()})
|
|
16444
|
-
Time elapsed: ${timeDiff} seconds
|
|
16445
|
-
Signature quantum: ${sigQuantum2}
|
|
16446
|
-
Current quantum: ${currentQuantum}
|
|
16447
|
-
Quantum window: 60 seconds
|
|
16448
|
-
Status: ${sigQuantum2 < currentQuantum ? "\u274C EXPIRED - Signature created in older quantum window" : "\u2705 VALID"}
|
|
16449
|
-
Note: QuantumOutsideWindow can also occur if the tx is mined outside the accepted past/current/next quantum set.`;
|
|
16450
|
-
}
|
|
16451
|
-
if (!isDeadZonePreflightArtifact) {
|
|
16452
|
-
throw new Error(
|
|
16453
|
-
`Preflight check failed - mintUCD would revert:
|
|
16454
|
-
Error: ${errorName}
|
|
16455
|
-
Selector: ${selector}
|
|
16456
|
-
Message: ${errorMessage}
|
|
16457
|
-
Data: ${errorData}${quantumContext}${mintDebtDiagnostics}`
|
|
16458
|
-
);
|
|
16459
|
-
}
|
|
16460
|
-
} else {
|
|
16461
|
-
const currentTime2 = Math.floor(Date.now() / 1e3);
|
|
16462
|
-
const timeSinceSig = currentTime2 - validationResponse.timestamp;
|
|
16463
|
-
const currentQuantum = Math.floor(currentTime2 / 60) * 60;
|
|
16464
|
-
const sigQuantum2 = Math.floor(validationResponse.timestamp / 60) * 60;
|
|
16465
|
-
throw new Error(
|
|
16466
|
-
`Preflight check failed - mintUCD would revert:
|
|
16467
|
-
${errorMessage}
|
|
16468
|
-
|
|
16469
|
-
Error Details:
|
|
16470
|
-
Error data: ${errorData || "0x (empty - no custom error)"}
|
|
16471
|
-
Error code: ${preflightError?.code || "none"}
|
|
16472
|
-
|
|
16473
|
-
Quantum Timing Analysis:
|
|
16474
|
-
Signature created: ${validationResponse.timestamp} (${new Date(
|
|
16475
|
-
validationResponse.timestamp * 1e3
|
|
16476
|
-
).toISOString()})
|
|
16477
|
-
Current time: ${currentTime2} (${new Date(
|
|
16478
|
-
currentTime2 * 1e3
|
|
16479
|
-
).toISOString()})
|
|
16480
|
-
Time elapsed: ${timeSinceSig} seconds
|
|
16481
|
-
Signature quantum: ${sigQuantum2}
|
|
16482
|
-
Current quantum: ${currentQuantum}
|
|
16483
|
-
Status: ${sigQuantum2 < currentQuantum ? "\u274C EXPIRED - Signature too old" : "\u2705 VALID"}
|
|
16484
|
-
|
|
16485
|
-
Possible Causes (when error data is empty):
|
|
16486
|
-
\u2022 Quantum signature expired (current=${currentQuantum}, signature=${sigQuantum2})
|
|
16487
|
-
\u2022 Contract paused or circuit breaker active
|
|
16488
|
-
\u2022 Position state changed since LIT Action signed
|
|
16489
|
-
\u2022 Authorization registry validation failed
|
|
16490
|
-
\u2022 RPC provider not returning detailed revert data
|
|
16491
|
-
|
|
16492
|
-
Position: ${request.positionId}`
|
|
16493
|
-
);
|
|
16494
|
-
}
|
|
17353
|
+
log.info(" === LIT Action Response ===", {});
|
|
17354
|
+
log.info(
|
|
17355
|
+
` authorizedSpendsHash: ${validationResponse.authorizedSpendsHash}`,
|
|
17356
|
+
{}
|
|
17357
|
+
);
|
|
17358
|
+
log.info(` mintAmount: ${validationResponse.mintAmount}`, {});
|
|
17359
|
+
log.info(` mintFee: ${validationResponse.mintFee}`, {});
|
|
17360
|
+
log.info(` newDebt: ${validationResponse.newDebt}`, {});
|
|
17361
|
+
log.info(` newCollateral: ${validationResponse.newCollateral}`, {});
|
|
17362
|
+
log.info(` btcPrice: ${validationResponse.btcPrice}`, {});
|
|
17363
|
+
log.info(
|
|
17364
|
+
` timestamp (quantumTimestamp): ${validationResponse.timestamp}`,
|
|
17365
|
+
{}
|
|
17366
|
+
);
|
|
17367
|
+
log.info(" === SDK Calculated Hashes ===", {});
|
|
17368
|
+
log.info(` Current UCD Debt (from position): ${currentDebt}`, {});
|
|
17369
|
+
log.info(` ucdDebtHash: ${ucdDebtHash}`, {});
|
|
17370
|
+
log.info(" === Contract Addresses for contractHash ===", {});
|
|
17371
|
+
log.info(` PositionManager: ${positionManagerAddress}`, {});
|
|
17372
|
+
log.info(
|
|
17373
|
+
` LoanOpsManager: ${this.getContractAddressesOrThrow().loanOperationsManager}`,
|
|
17374
|
+
{}
|
|
17375
|
+
);
|
|
17376
|
+
log.info(
|
|
17377
|
+
` TermManager: ${this.getContractAddressesOrThrow().termManager}`,
|
|
17378
|
+
{}
|
|
17379
|
+
);
|
|
17380
|
+
log.info(
|
|
17381
|
+
` UCDController: ${this.getContractAddressesOrThrow().ucdController}`,
|
|
17382
|
+
{}
|
|
17383
|
+
);
|
|
17384
|
+
log.info(` contractHash: ${contractHash}`, {});
|
|
17385
|
+
log.info(" === Signature ===", {});
|
|
17386
|
+
log.info(
|
|
17387
|
+
` Signature (first 20 chars): ${finalSignature.substring(
|
|
17388
|
+
0,
|
|
17389
|
+
20
|
|
17390
|
+
)}...`,
|
|
17391
|
+
{}
|
|
17392
|
+
);
|
|
17393
|
+
log.info(` Signature length: ${finalSignature.length}`, {});
|
|
17394
|
+
log.info(
|
|
17395
|
+
` Final signature (first 50 chars): ${finalSignature ? finalSignature.substring(0, 50) : "EMPTY"}`,
|
|
17396
|
+
{}
|
|
17397
|
+
);
|
|
17398
|
+
log.info(
|
|
17399
|
+
` Final signature is empty: ${!finalSignature || finalSignature.length === 0}`,
|
|
17400
|
+
{}
|
|
17401
|
+
);
|
|
16495
17402
|
}
|
|
16496
17403
|
try {
|
|
16497
17404
|
const coreAddress = this.getContractAddressesOrThrow().positionManagerCore;
|
|
@@ -16510,7 +17417,7 @@ Position: ${request.positionId}`
|
|
|
16510
17417
|
positionIdBytes32
|
|
16511
17418
|
);
|
|
16512
17419
|
const currentDebtHash = keccak2565(
|
|
16513
|
-
|
|
17420
|
+
AbiCoder2.defaultAbiCoder().encode(
|
|
16514
17421
|
["uint256"],
|
|
16515
17422
|
[currentPosition.ucdDebt]
|
|
16516
17423
|
)
|
|
@@ -16688,146 +17595,121 @@ Position: ${request.positionId}`
|
|
|
16688
17595
|
signatureHexMint
|
|
16689
17596
|
]);
|
|
16690
17597
|
const fromAddress = await signer.getAddress();
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
positionManagerAddress,
|
|
16695
|
-
mintCalldata,
|
|
16696
|
-
MINT_UCD_ESTIMATE_MARGIN_BPS,
|
|
16697
|
-
MINT_UCD_GAS_CEILING
|
|
16698
|
-
);
|
|
16699
|
-
let tx;
|
|
16700
|
-
if (estimatedGas !== null) {
|
|
16701
|
-
if (this.config.debug) {
|
|
16702
|
-
log.info(
|
|
16703
|
-
`\u{1F4DD} Broadcasting mintUCD (gasLimit=${estimatedGas.toString()})`,
|
|
16704
|
-
{}
|
|
16705
|
-
);
|
|
16706
|
-
}
|
|
16707
|
-
await awaitSafeSubmissionWindow(Number(validationResponse.timestamp));
|
|
16708
|
-
tx = await sendEip1559Transaction({
|
|
16709
|
-
signer,
|
|
17598
|
+
try {
|
|
17599
|
+
await assertSafeQuantumSubmission({
|
|
17600
|
+
provider: signerProvider,
|
|
16710
17601
|
to: positionManagerAddress,
|
|
17602
|
+
from: fromAddress,
|
|
16711
17603
|
data: mintCalldata,
|
|
16712
|
-
|
|
17604
|
+
quantumTimestamp: Number(validationResponse.timestamp),
|
|
17605
|
+
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
16713
17606
|
});
|
|
16714
|
-
}
|
|
16715
|
-
if (
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16731
|
-
|
|
16732
|
-
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
"0x70f4b32a": "UnauthorizedSigner()",
|
|
16751
|
-
"0x48f5c3ed": "Unauthorized()",
|
|
16752
|
-
"0x36bfdeea": "StateHashMismatch()",
|
|
16753
|
-
"0x131d9a21": "QuantumExpired()",
|
|
16754
|
-
"0x52ce5d58": "QuantumAlreadyUsed()",
|
|
16755
|
-
"0x137f3b70": "InDeadZone()",
|
|
16756
|
-
"0xbe4b82c1": "DeadZoneViolation()",
|
|
16757
|
-
"0x3e76a3c9": "QuantumOutsideWindow()",
|
|
16758
|
-
"0x62278171": "InvalidValidatorSignature()",
|
|
16759
|
-
"0xb9d419a7": "DebtUpdateVerificationFailed()",
|
|
16760
|
-
"0xc7e20553": "DebtUpdateVerificationFailedDetailed(bytes32,uint256,uint256,uint256,uint256,uint256)",
|
|
16761
|
-
"0x0cfd2a97": "MintVerificationMismatch(uint256,uint256)",
|
|
16762
|
-
"0x07637bd8": "MintFailed()"
|
|
16763
|
-
};
|
|
16764
|
-
const errorName = knownErrors[selector] || `Unknown error ${selector}`;
|
|
16765
|
-
throw new Error(
|
|
16766
|
-
`mintUCD reverted: ${errorName}
|
|
16767
|
-
Error selector: ${selector}
|
|
16768
|
-
Error data: ${errorData}
|
|
16769
|
-
Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
|
|
16770
|
-
Message: ${errorMessage}`
|
|
16771
|
-
);
|
|
16772
|
-
}
|
|
16773
|
-
if (errorData === "0x" || !errorData) {
|
|
16774
|
-
const viewsAddress = this.config.contractAddresses?.positionManagerViews;
|
|
16775
|
-
if (viewsAddress) {
|
|
16776
|
-
try {
|
|
16777
|
-
const views = new Contract6(
|
|
16778
|
-
viewsAddress,
|
|
16779
|
-
[
|
|
16780
|
-
"function diagnoseMintUCD(bytes32,uint256,uint256,uint256,uint256,uint256,bytes32,bytes32,bytes32,uint256,bytes) view returns (string)"
|
|
16781
|
-
],
|
|
16782
|
-
signerProvider
|
|
16783
|
-
);
|
|
16784
|
-
const diagnosis = await views["diagnoseMintUCD"](
|
|
16785
|
-
request.positionId,
|
|
16786
|
-
validationResponse.mintAmount,
|
|
16787
|
-
validationResponse.mintFee,
|
|
16788
|
-
validationResponse.newDebt,
|
|
16789
|
-
validationResponse.newCollateral,
|
|
16790
|
-
validationResponse.btcPrice,
|
|
16791
|
-
validationResponse.authorizedSpendsHash,
|
|
16792
|
-
ucdDebtHash,
|
|
16793
|
-
contractHash,
|
|
16794
|
-
validationResponse.timestamp,
|
|
16795
|
-
signatureHexMint
|
|
16796
|
-
);
|
|
16797
|
-
throw new Error(
|
|
16798
|
-
`mintUCD reverted with empty data; diagnosis: ${diagnosis}`
|
|
17607
|
+
} catch (simError) {
|
|
17608
|
+
if (!(simError instanceof QuantumRevertError)) {
|
|
17609
|
+
throw simError;
|
|
17610
|
+
}
|
|
17611
|
+
const simCause = simError.cause;
|
|
17612
|
+
const causeMessage = simCause?.reason ?? simCause?.shortMessage ?? (simCause instanceof Error ? simCause.message : String(simCause));
|
|
17613
|
+
if (!simError.data) {
|
|
17614
|
+
const viewsAddress = this.config.contractAddresses?.positionManagerViews;
|
|
17615
|
+
if (viewsAddress) {
|
|
17616
|
+
let diagnosis = null;
|
|
17617
|
+
try {
|
|
17618
|
+
const views = new Contract6(
|
|
17619
|
+
viewsAddress,
|
|
17620
|
+
[
|
|
17621
|
+
"function diagnoseMintUCD(bytes32,uint256,uint256,uint256,uint256,uint256,bytes32,bytes32,bytes32,uint256,bytes) view returns (string)"
|
|
17622
|
+
],
|
|
17623
|
+
signerProvider
|
|
17624
|
+
);
|
|
17625
|
+
diagnosis = await views["diagnoseMintUCD"](
|
|
17626
|
+
request.positionId,
|
|
17627
|
+
validationResponse.mintAmount,
|
|
17628
|
+
validationResponse.mintFee,
|
|
17629
|
+
validationResponse.newDebt,
|
|
17630
|
+
validationResponse.newCollateral,
|
|
17631
|
+
validationResponse.btcPrice,
|
|
17632
|
+
validationResponse.authorizedSpendsHash,
|
|
17633
|
+
ucdDebtHash,
|
|
17634
|
+
contractHash,
|
|
17635
|
+
validationResponse.timestamp,
|
|
17636
|
+
signatureHexMint
|
|
17637
|
+
);
|
|
17638
|
+
} catch (diagErr) {
|
|
17639
|
+
if (this.config.debug) {
|
|
17640
|
+
log.info(
|
|
17641
|
+
` diagnoseMintUCD unavailable or failed: ${diagErr?.message || diagErr}`,
|
|
17642
|
+
{}
|
|
16799
17643
|
);
|
|
16800
|
-
} catch (diagErr) {
|
|
16801
|
-
if (this.config.debug) {
|
|
16802
|
-
log.info(
|
|
16803
|
-
` diagnoseMintUCD unavailable or failed: ${diagErr?.message || diagErr}`,
|
|
16804
|
-
{}
|
|
16805
|
-
);
|
|
16806
|
-
}
|
|
16807
17644
|
}
|
|
16808
17645
|
}
|
|
17646
|
+
if (diagnosis !== null) {
|
|
17647
|
+
throw new Error(
|
|
17648
|
+
`mintUCD reverted with empty data; diagnosis: ${diagnosis}`
|
|
17649
|
+
);
|
|
17650
|
+
}
|
|
16809
17651
|
}
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
Error data: ${errorData || "none"}`
|
|
16814
|
-
);
|
|
16815
|
-
}
|
|
16816
|
-
}
|
|
16817
|
-
if (this.config.debug) {
|
|
16818
|
-
log.info(
|
|
16819
|
-
"\u2705 Static call clear \u2014 broadcasting mintUCD with fixed gas ceiling",
|
|
16820
|
-
{}
|
|
17652
|
+
throw new Error(
|
|
17653
|
+
`mintUCD would revert (pre-send simulation): ${causeMessage}
|
|
17654
|
+
Error data: none`
|
|
16821
17655
|
);
|
|
16822
17656
|
}
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
17657
|
+
const selector = simError.selector;
|
|
17658
|
+
const errorName = simError.errorName ?? (selector ? `Unknown error ${selector}` : "unknown");
|
|
17659
|
+
const mintDebtDiagnostics = selector === "0xb9d419a7" || // DebtUpdateVerificationFailed()
|
|
17660
|
+
selector === "0xc7e20553" || // DebtUpdateVerificationFailedDetailed(...)
|
|
17661
|
+
selector === "0x0cfd2a97" || // MintVerificationMismatch(uint256,uint256)
|
|
17662
|
+
selector === "0x07637bd8" ? `
|
|
17663
|
+
|
|
17664
|
+
Mint/debt diagnostics (LoanOperationsManager mint path \u2192 increaseDebtFromMint):
|
|
17665
|
+
positionId: ${request.positionId}
|
|
17666
|
+
on-chain ucdDebt before tx: ${currentDebt}
|
|
17667
|
+
Lit newDebt: ${validationResponse.newDebt}
|
|
17668
|
+
Lit mintAmount: ${validationResponse.mintAmount}
|
|
17669
|
+
Lit mintFee: ${validationResponse.mintFee}
|
|
17670
|
+
Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
|
|
17671
|
+
Contract expects UCD supply increase to match mintAmount+mintFee and debt update to newDebt.
|
|
17672
|
+
` : "";
|
|
17673
|
+
let quantumContext = "";
|
|
17674
|
+
if (selector === "0x131d9a21" || // QuantumExpired()
|
|
17675
|
+
selector === "0x52ce5d58" || // QuantumAlreadyUsed()
|
|
17676
|
+
selector === "0x137f3b70" || // InDeadZone()
|
|
17677
|
+
selector === "0xbe4b82c1" || // DeadZoneViolation()
|
|
17678
|
+
selector === "0x3e76a3c9") {
|
|
17679
|
+
const nowSecs = Math.floor(Date.now() / 1e3);
|
|
17680
|
+
const nowQuantum = Math.floor(nowSecs / 60) * 60;
|
|
17681
|
+
const signedQuantum = Math.floor(Number(validationResponse.timestamp) / 60) * 60;
|
|
17682
|
+
const timeDiff = nowSecs - Number(validationResponse.timestamp);
|
|
17683
|
+
quantumContext = `
|
|
17684
|
+
|
|
17685
|
+
Quantum Timing Analysis:
|
|
17686
|
+
Signature created: ${validationResponse.timestamp} (${new Date(
|
|
17687
|
+
Number(validationResponse.timestamp) * 1e3
|
|
17688
|
+
).toISOString()})
|
|
17689
|
+
Current time: ${nowSecs} (${new Date(
|
|
17690
|
+
nowSecs * 1e3
|
|
17691
|
+
).toISOString()})
|
|
17692
|
+
Time elapsed: ${timeDiff} seconds
|
|
17693
|
+
Signature quantum: ${signedQuantum}
|
|
17694
|
+
Current quantum: ${nowQuantum}
|
|
17695
|
+
Quantum window: 60 seconds
|
|
17696
|
+
Status: ${signedQuantum < nowQuantum ? "\u274C EXPIRED - Signature created in older quantum window" : "\u2705 VALID"}
|
|
17697
|
+
Note: QuantumOutsideWindow can also occur if the tx is mined outside the accepted past/current/next quantum set.`;
|
|
17698
|
+
}
|
|
17699
|
+
throw new Error(
|
|
17700
|
+
`mintUCD would revert (pre-send simulation): ${errorName}
|
|
17701
|
+
Error selector: ${selector}
|
|
17702
|
+
Error data: ${simError.data}
|
|
17703
|
+
Call path: mintUCD -> mintUCDWithAuthorization -> increaseDebtFromMint -> finalizeMintDebtIncrease
|
|
17704
|
+
Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
17705
|
+
);
|
|
16830
17706
|
}
|
|
17707
|
+
const tx = await sendEip1559Transaction({
|
|
17708
|
+
signer,
|
|
17709
|
+
to: positionManagerAddress,
|
|
17710
|
+
data: mintCalldata,
|
|
17711
|
+
gasLimit: MINT_UCD_GAS_CEILING
|
|
17712
|
+
});
|
|
16831
17713
|
const receipt = await signerProvider.waitForTransaction(
|
|
16832
17714
|
tx.hash,
|
|
16833
17715
|
1,
|
|
@@ -17913,9 +18795,12 @@ Error data: ${errorData || "none"}`
|
|
|
17913
18795
|
userSignature: auth.signature,
|
|
17914
18796
|
rpcUrl: _rpcUrl || this.config.ethRpcUrl,
|
|
17915
18797
|
// Forward a Bitcoin provider so the validator reaches a reachable esplora on Hardhat
|
|
17916
|
-
// (lit-ops maps it to the
|
|
18798
|
+
// Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
|
|
18799
|
+
// validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
|
|
18800
|
+
// is resolved from the on-chain BitcoinProviderRegistry and this field is server-ignored
|
|
18801
|
+
// — emitting it there only misleads readers into thinking it is caller-controlled. Prefer
|
|
17917
18802
|
// a per-request override (mirrors the mint path's customBitcoinRpcUrl) over the config default.
|
|
17918
|
-
...(_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
|
|
18803
|
+
...this.config.chain === "hardhat" && (_bitcoinRpcUrl || this.config.bitcoinProviders?.[0]?.url) && {
|
|
17919
18804
|
bitcoinProviderUrl: _bitcoinRpcUrl || this.config.bitcoinProviders[0].url
|
|
17920
18805
|
},
|
|
17921
18806
|
...this.config.priceProviders && {
|
|
@@ -17991,26 +18876,43 @@ Error data: ${errorData || "none"}`
|
|
|
17991
18876
|
if (!signatureBytes.startsWith("0x")) {
|
|
17992
18877
|
signatureBytes = "0x" + signatureBytes;
|
|
17993
18878
|
}
|
|
17994
|
-
const
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
`Failed to get PositionManager: ${positionManagerResult.error.message}`
|
|
17999
|
-
);
|
|
18000
|
-
}
|
|
18001
|
-
const positionManager = positionManagerResult.value;
|
|
18002
|
-
await awaitSafeSubmissionWindow(Number(quantumTimestamp));
|
|
18003
|
-
const extendPositionFn = positionManager["extendPosition"];
|
|
18004
|
-
const tx = await extendPositionFn(
|
|
18879
|
+
const extendIface = new Interface4([
|
|
18880
|
+
"function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, uint256 proRataRenewalFee, bytes calldata extensionValidatorSignature) external returns (bool)"
|
|
18881
|
+
]);
|
|
18882
|
+
const extendCalldata = extendIface.encodeFunctionData("extendPosition", [
|
|
18005
18883
|
positionIdBytes32,
|
|
18006
18884
|
BigInt(_selectedTerm),
|
|
18007
18885
|
quantumTimestamp,
|
|
18008
18886
|
btcPrice,
|
|
18009
18887
|
availableBTCBalance,
|
|
18010
18888
|
proRataRenewalFee,
|
|
18011
|
-
signatureBytes
|
|
18012
|
-
|
|
18013
|
-
);
|
|
18889
|
+
signatureBytes
|
|
18890
|
+
]);
|
|
18891
|
+
const extendTo = this.getContractAddressesOrThrow().positionManager;
|
|
18892
|
+
const extendSigner = this.getSignerOrThrow();
|
|
18893
|
+
try {
|
|
18894
|
+
await assertSafeQuantumSubmission({
|
|
18895
|
+
provider: this.getProviderOrThrow(),
|
|
18896
|
+
to: extendTo,
|
|
18897
|
+
from: await extendSigner.getAddress(),
|
|
18898
|
+
data: extendCalldata,
|
|
18899
|
+
quantumTimestamp: Number(quantumTimestamp),
|
|
18900
|
+
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
18901
|
+
});
|
|
18902
|
+
} catch (simError) {
|
|
18903
|
+
if (!(simError instanceof QuantumRevertError)) {
|
|
18904
|
+
throw simError;
|
|
18905
|
+
}
|
|
18906
|
+
throw new Error(
|
|
18907
|
+
`extendPosition would revert (pre-send simulation): ${simError.errorName ?? simError.selector ?? "unknown"}`
|
|
18908
|
+
);
|
|
18909
|
+
}
|
|
18910
|
+
const tx = await sendEip1559Transaction({
|
|
18911
|
+
signer: extendSigner,
|
|
18912
|
+
to: extendTo,
|
|
18913
|
+
data: extendCalldata,
|
|
18914
|
+
gasLimit: EXTEND_POSITION_GAS_CEILING
|
|
18915
|
+
});
|
|
18014
18916
|
const txWithProvider = tx;
|
|
18015
18917
|
if (!txWithProvider.provider) {
|
|
18016
18918
|
const signer2 = this.getSignerOrThrow();
|
|
@@ -18679,9 +19581,10 @@ Error data: ${errorData || "none"}`
|
|
|
18679
19581
|
rpcUrl: this.config.ethRpcUrl,
|
|
18680
19582
|
// Pass the SDK's RPC URL to LIT Action
|
|
18681
19583
|
...request.rpcUrl && { rpcUrl: request.rpcUrl },
|
|
18682
|
-
//
|
|
18683
|
-
//
|
|
18684
|
-
|
|
19584
|
+
// Hardhat-ONLY: gives the validator a reachable esplora (lit-ops maps it to the
|
|
19585
|
+
// validator's devBitcoinProviderUrl). Never emitted on prod chains, where the provider
|
|
19586
|
+
// comes from the on-chain BitcoinProviderRegistry and this field is server-ignored.
|
|
19587
|
+
...this.config.chain === "hardhat" && this.config.bitcoinProviders?.[0]?.url && {
|
|
18685
19588
|
bitcoinProviderUrl: this.config.bitcoinProviders[0].url
|
|
18686
19589
|
},
|
|
18687
19590
|
...this.config.priceProviders && {
|
|
@@ -18870,7 +19773,6 @@ Error data: ${errorData || "none"}`
|
|
|
18870
19773
|
quantumTimestamp: quantumTimestamp.toString()
|
|
18871
19774
|
});
|
|
18872
19775
|
}
|
|
18873
|
-
await awaitSafeSubmissionWindow(Number(quantumTimestamp));
|
|
18874
19776
|
const paymentAmountStr = paymentAmountWei.toString();
|
|
18875
19777
|
const quantumTimestampStr = quantumTimestamp.toString();
|
|
18876
19778
|
const btcPriceStr = btcPrice.toString();
|
|
@@ -18888,58 +19790,30 @@ Error data: ${errorData || "none"}`
|
|
|
18888
19790
|
btcPriceStr,
|
|
18889
19791
|
signature
|
|
18890
19792
|
]);
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
if (decoded === "DeadZoneViolation()" && sim <= MAX_DEADZONE_RESIMULATIONS) {
|
|
18909
|
-
const nowSec = Math.floor(Date.now() / 1e3);
|
|
18910
|
-
const sigQuantum = Math.floor(Number(quantumTimestamp) / 60) * 60;
|
|
18911
|
-
const realCurrentQuantum = Math.floor(nowSec / 60) * 60;
|
|
18912
|
-
if (sigQuantum === realCurrentQuantum) {
|
|
18913
|
-
if (this.config.debug) {
|
|
18914
|
-
log.info(
|
|
18915
|
-
"\u21AA\uFE0F Simulated DeadZoneViolation is a stale-latest-block artifact (signature is the current quantum) \u2014 proceeding to broadcast",
|
|
18916
|
-
{}
|
|
18917
|
-
);
|
|
18918
|
-
}
|
|
18919
|
-
break;
|
|
18920
|
-
}
|
|
18921
|
-
const gate = await awaitSafeSubmissionWindow(Number(quantumTimestamp));
|
|
18922
|
-
if (gate.waited) {
|
|
18923
|
-
if (this.config.debug) {
|
|
18924
|
-
log.info(
|
|
18925
|
-
`\u{1F501} Re-simulating makePayment after crossing the quantum boundary (attempt ${sim}/${MAX_DEADZONE_RESIMULATIONS})`,
|
|
18926
|
-
{}
|
|
18927
|
-
);
|
|
18928
|
-
}
|
|
18929
|
-
continue;
|
|
18930
|
-
}
|
|
18931
|
-
}
|
|
18932
|
-
const label = decoded ?? simError?.reason ?? simError?.shortMessage ?? (simError instanceof Error ? simError.message : String(simError));
|
|
18933
|
-
if (this.config.debug) {
|
|
18934
|
-
log.error("\u274C makePayment pre-send simulation reverted", { error: label });
|
|
18935
|
-
}
|
|
18936
|
-
return {
|
|
18937
|
-
success: false,
|
|
18938
|
-
error: `Payment would revert (pre-send simulation): ${label}`,
|
|
18939
|
-
positionId: request.positionId,
|
|
18940
|
-
paymentAmountUCD: request.paymentAmount
|
|
18941
|
-
};
|
|
19793
|
+
try {
|
|
19794
|
+
await assertSafeQuantumSubmission({
|
|
19795
|
+
provider: paymentProvider,
|
|
19796
|
+
to: paymentContractAddress,
|
|
19797
|
+
from: paymentFrom,
|
|
19798
|
+
data: paymentCalldata,
|
|
19799
|
+
quantumTimestamp: Number(quantumTimestamp),
|
|
19800
|
+
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
19801
|
+
});
|
|
19802
|
+
} catch (simError) {
|
|
19803
|
+
if (!(simError instanceof QuantumRevertError)) {
|
|
19804
|
+
throw simError;
|
|
19805
|
+
}
|
|
19806
|
+
const simCause = simError.cause;
|
|
19807
|
+
const label = simError.errorName ?? (simError.selector ? `Unknown error ${simError.selector}` : null) ?? simCause?.reason ?? simCause?.shortMessage ?? (simCause instanceof Error ? simCause.message : String(simCause));
|
|
19808
|
+
if (this.config.debug) {
|
|
19809
|
+
log.error("\u274C makePayment pre-send simulation reverted", { error: label });
|
|
18942
19810
|
}
|
|
19811
|
+
return {
|
|
19812
|
+
success: false,
|
|
19813
|
+
error: `Payment would revert (pre-send simulation): ${label}`,
|
|
19814
|
+
positionId: request.positionId,
|
|
19815
|
+
paymentAmountUCD: request.paymentAmount
|
|
19816
|
+
};
|
|
18943
19817
|
}
|
|
18944
19818
|
const tx = await sendEip1559Transaction({
|
|
18945
19819
|
signer: paymentSigner,
|
|
@@ -19613,13 +20487,6 @@ Error data: ${errorData || "none"}`
|
|
|
19613
20487
|
error: "PositionManager address not configured"
|
|
19614
20488
|
};
|
|
19615
20489
|
}
|
|
19616
|
-
const positionManagerContract = new Contract6(
|
|
19617
|
-
positionManagerAddress,
|
|
19618
|
-
[
|
|
19619
|
-
"function withdrawBTC((bytes32 positionId, bytes32 actionHash, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractBundleHash, string withdrawalAddress, uint256 totalDeduction, uint256 newCollateral, uint256 quantumTimestamp, uint256 btcPrice, string utxoTxid, uint32 utxoVout) params, bytes withdrawalValidatorSignature, bytes btcSpendAuthSignature) external returns (bool)"
|
|
19620
|
-
],
|
|
19621
|
-
this.getSignerOrThrow()
|
|
19622
|
-
);
|
|
19623
20490
|
const withdrawalParams = {
|
|
19624
20491
|
positionId: positionIdBytes32,
|
|
19625
20492
|
actionHash: validationResponse.actionHash,
|
|
@@ -19652,86 +20519,56 @@ Error data: ${errorData || "none"}`
|
|
|
19652
20519
|
utxoVout: withdrawalParams.utxoVout
|
|
19653
20520
|
});
|
|
19654
20521
|
}
|
|
19655
|
-
|
|
20522
|
+
const withdrawIface = new Interface4([
|
|
20523
|
+
"function withdrawBTC((bytes32 positionId, bytes32 actionHash, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractBundleHash, string withdrawalAddress, uint256 totalDeduction, uint256 newCollateral, uint256 quantumTimestamp, uint256 btcPrice, string utxoTxid, uint32 utxoVout) params, bytes withdrawalValidatorSignature, bytes btcSpendAuthSignature) external returns (bool)"
|
|
20524
|
+
]);
|
|
20525
|
+
const withdrawCalldata = withdrawIface.encodeFunctionData("withdrawBTC", [
|
|
20526
|
+
withdrawalParams,
|
|
20527
|
+
signatureBytes,
|
|
20528
|
+
btcSpendAuthBytes
|
|
20529
|
+
]);
|
|
20530
|
+
const withdrawSigner = this.getSignerOrThrow();
|
|
19656
20531
|
let tx;
|
|
19657
20532
|
try {
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
|
|
20533
|
+
await assertSafeQuantumSubmission({
|
|
20534
|
+
provider: this.getProviderOrThrow(),
|
|
20535
|
+
to: positionManagerAddress,
|
|
20536
|
+
from: await withdrawSigner.getAddress(),
|
|
20537
|
+
data: withdrawCalldata,
|
|
20538
|
+
quantumTimestamp: Number(withdrawalParams.quantumTimestamp),
|
|
20539
|
+
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
20540
|
+
});
|
|
20541
|
+
tx = await sendEip1559Transaction({
|
|
20542
|
+
signer: withdrawSigner,
|
|
20543
|
+
to: positionManagerAddress,
|
|
20544
|
+
data: withdrawCalldata,
|
|
20545
|
+
gasLimit: WITHDRAW_BTC_GAS_CEILING
|
|
20546
|
+
});
|
|
19664
20547
|
} catch (txError) {
|
|
19665
|
-
const txErrorMsg = txError instanceof Error ? txError.message : String(txError);
|
|
20548
|
+
const txErrorMsg = txError instanceof QuantumRevertError ? `Withdrawal would revert (pre-send simulation): ${txError.errorName ?? txError.selector ?? "unknown"}` : txError instanceof Error ? txError.message : String(txError);
|
|
19666
20549
|
if (this.config.debug) {
|
|
19667
|
-
log.
|
|
19668
|
-
|
|
19669
|
-
|
|
19670
|
-
|
|
20550
|
+
log.error("\u274C withdrawBTC transaction submission failed", {
|
|
20551
|
+
error: txErrorMsg,
|
|
20552
|
+
withdrawalParams,
|
|
20553
|
+
signatureBytesLength: signatureBytes.length
|
|
20554
|
+
});
|
|
19671
20555
|
}
|
|
19672
20556
|
try {
|
|
19673
|
-
const
|
|
19674
|
-
const
|
|
19675
|
-
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
btcSpendAuthBytes
|
|
19681
|
-
]);
|
|
19682
|
-
const signer = this.getSignerOrThrow();
|
|
19683
|
-
const fromAddress = await signer.getAddress();
|
|
19684
|
-
const estimatedGas = await estimateContractCallGasWithMargin(
|
|
19685
|
-
this.getProviderOrThrow(),
|
|
19686
|
-
fromAddress,
|
|
19687
|
-
contractAddress,
|
|
19688
|
-
calldata,
|
|
19689
|
-
MINT_UCD_ESTIMATE_MARGIN_BPS,
|
|
19690
|
-
WITHDRAW_BTC_GAS_CEILING
|
|
19691
|
-
);
|
|
19692
|
-
const gasLimit = estimatedGas ?? WITHDRAW_BTC_GAS_CEILING;
|
|
19693
|
-
await awaitSafeSubmissionWindow(
|
|
19694
|
-
Number(withdrawalParams.quantumTimestamp)
|
|
20557
|
+
const position = await this.getPosition(positionId);
|
|
20558
|
+
const btcAmount = (Number(withdrawalAmount) / 1e8).toFixed(8);
|
|
20559
|
+
const message = formatBTCWithdrawalFailureMessage(
|
|
20560
|
+
btcAmount,
|
|
20561
|
+
position?.vaultAddress || "unknown",
|
|
20562
|
+
withdrawalAddress,
|
|
20563
|
+
positionId
|
|
19695
20564
|
);
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
data: calldata,
|
|
19699
|
-
value: "0x0",
|
|
19700
|
-
gasLimit
|
|
19701
|
-
});
|
|
19702
|
-
if (this.config.debug) {
|
|
19703
|
-
log.info("\u2705 withdrawBTC raw transaction fallback succeeded", {
|
|
19704
|
-
txHash: tx.hash,
|
|
19705
|
-
gasLimit: gasLimit.toString(),
|
|
19706
|
-
estimated: estimatedGas !== null
|
|
19707
|
-
});
|
|
19708
|
-
}
|
|
19709
|
-
} catch (rawError) {
|
|
19710
|
-
const rawErrorMsg = rawError instanceof Error ? rawError.message : String(rawError);
|
|
19711
|
-
if (this.config.debug) {
|
|
19712
|
-
log.error("\u274C withdrawBTC transaction failed (raw fallback also failed)", {
|
|
19713
|
-
error: rawErrorMsg,
|
|
19714
|
-
withdrawalParams,
|
|
19715
|
-
signatureBytesLength: signatureBytes.length
|
|
19716
|
-
});
|
|
19717
|
-
}
|
|
19718
|
-
try {
|
|
19719
|
-
const position = await this.getPosition(positionId);
|
|
19720
|
-
const btcAmount = (Number(withdrawalAmount) / 1e8).toFixed(8);
|
|
19721
|
-
const message = formatBTCWithdrawalFailureMessage(
|
|
19722
|
-
btcAmount,
|
|
19723
|
-
position?.vaultAddress || "unknown",
|
|
19724
|
-
withdrawalAddress,
|
|
19725
|
-
positionId
|
|
19726
|
-
);
|
|
19727
|
-
await this._relayNotification(message);
|
|
19728
|
-
} catch {
|
|
19729
|
-
}
|
|
19730
|
-
return {
|
|
19731
|
-
success: false,
|
|
19732
|
-
error: `Transaction submission failed: ${rawErrorMsg}`
|
|
19733
|
-
};
|
|
20565
|
+
await this._relayNotification(message);
|
|
20566
|
+
} catch {
|
|
19734
20567
|
}
|
|
20568
|
+
return {
|
|
20569
|
+
success: false,
|
|
20570
|
+
error: `Transaction submission failed: ${txErrorMsg}`
|
|
20571
|
+
};
|
|
19735
20572
|
}
|
|
19736
20573
|
if (this.config.debug) {
|
|
19737
20574
|
log.info("\u2705 Transaction submitted, waiting for confirmation:", {
|
|
@@ -19754,6 +20591,12 @@ Error data: ${errorData || "none"}`
|
|
|
19754
20591
|
error: `Transaction confirmation failed: ${waitErrorMsg}`
|
|
19755
20592
|
};
|
|
19756
20593
|
}
|
|
20594
|
+
if (!receipt) {
|
|
20595
|
+
return {
|
|
20596
|
+
success: false,
|
|
20597
|
+
error: `Transaction confirmation returned no receipt. Hash: ${tx.hash}`
|
|
20598
|
+
};
|
|
20599
|
+
}
|
|
19757
20600
|
if (this.config.debug) {
|
|
19758
20601
|
log.info("\u2705 BTC withdrawal confirmed", {
|
|
19759
20602
|
blockNumber: receipt.blockNumber,
|
|
@@ -19869,7 +20712,8 @@ Error data: ${errorData || "none"}`
|
|
|
19869
20712
|
targetAmount: request.targetAmount,
|
|
19870
20713
|
networkFee: request.networkFee,
|
|
19871
20714
|
chainId,
|
|
19872
|
-
signer
|
|
20715
|
+
signer,
|
|
20716
|
+
...request.utxos ? { utxos: request.utxos } : {}
|
|
19873
20717
|
});
|
|
19874
20718
|
const headers = {
|
|
19875
20719
|
"Content-Type": "application/json",
|
|
@@ -19900,7 +20744,10 @@ Error data: ${errorData || "none"}`
|
|
|
19900
20744
|
userSignature: envelope.userSignature,
|
|
19901
20745
|
borrowerAddress: envelope.borrowerAddress,
|
|
19902
20746
|
chainId: envelope.chainId,
|
|
19903
|
-
timestamp: envelope.timestamp
|
|
20747
|
+
timestamp: envelope.timestamp,
|
|
20748
|
+
// P6/#8: the exact input set hashed into the user signature.
|
|
20749
|
+
// The server must forward this set verbatim to the signer.
|
|
20750
|
+
utxos: envelope.utxos
|
|
19904
20751
|
})
|
|
19905
20752
|
}
|
|
19906
20753
|
);
|
|
@@ -20240,10 +21087,13 @@ Error data: ${errorData || "none"}`
|
|
|
20240
21087
|
}
|
|
20241
21088
|
},
|
|
20242
21089
|
rpcUrl,
|
|
20243
|
-
|
|
21090
|
+
// Hardhat-ONLY: never emitted on prod chains (server-ignored there; provider comes
|
|
21091
|
+
// from the on-chain BitcoinProviderRegistry). The stale-spend action reads
|
|
21092
|
+
// `devBitcoinProviderUrl` directly on Hardhat — this legacy alias is redundant but
|
|
21093
|
+
// kept behind the Hardhat gate so no prod request carries the caller-looking field.
|
|
21094
|
+
...this.config.chain === "hardhat" && sdkBitcoinProviderUrl && { bitcoinProviderUrl: sdkBitcoinProviderUrl },
|
|
20244
21095
|
// Hardhat (chainId 1337/31337) has no on-chain BitcoinProviderRegistry —
|
|
20245
|
-
// Lit Actions read `devBitcoinProviderUrl` from globalThis.
|
|
20246
|
-
// non-hardhat chains ignore it. (Single source of truth: bitcoinProviderUrl.)
|
|
21096
|
+
// Lit Actions read `devBitcoinProviderUrl` from globalThis. Non-hardhat chains ignore it.
|
|
20247
21097
|
...sdkBitcoinProviderUrl && { devBitcoinProviderUrl: sdkBitcoinProviderUrl },
|
|
20248
21098
|
...params.minConfirmations !== void 0 && {
|
|
20249
21099
|
minConfirmations: params.minConfirmations
|
|
@@ -20566,6 +21416,15 @@ Error data: ${errorData || "none"}`
|
|
|
20566
21416
|
async getLoanEvents(positionId, filter) {
|
|
20567
21417
|
return this.loanQuery.getLoanEvents(positionId, filter);
|
|
20568
21418
|
}
|
|
21419
|
+
/**
|
|
21420
|
+
* Get loan-event history for every position owned by a wallet.
|
|
21421
|
+
* Composes getUserPositions + getLoanEvents; returns one LoanEvents per
|
|
21422
|
+
* position (each carrying its own positionId). Bitcoin deposits are not on the
|
|
21423
|
+
* subgraph — merge those in from an Esplora source on the consumer side.
|
|
21424
|
+
*/
|
|
21425
|
+
async getWalletLoanEvents(userAddress, filter) {
|
|
21426
|
+
return this.loanQuery.getWalletLoanEvents(userAddress, filter);
|
|
21427
|
+
}
|
|
20569
21428
|
/**
|
|
20570
21429
|
* Get protocol-wide events across all indexed entity types.
|
|
20571
21430
|
* Returns a merged, sorted discriminated-union timeline.
|
|
@@ -21022,6 +21881,38 @@ Error data: ${errorData || "none"}`
|
|
|
21022
21881
|
}));
|
|
21023
21882
|
}
|
|
21024
21883
|
}
|
|
21884
|
+
/**
|
|
21885
|
+
* List a vault address's PENDING (not-yet-confirmed) incoming BTC deposits.
|
|
21886
|
+
*
|
|
21887
|
+
* A deposit is "pending" until it reaches the protocol's minimum confirmation
|
|
21888
|
+
* depth (6) — the confirmed balance endpoints (`getVaultBalance` /
|
|
21889
|
+
* `getAddressBalance`) omit anything shallower, so this surfaces in-flight
|
|
21890
|
+
* deposits (0–5 confirmations). Server-delegated: lit-ops-server queries its
|
|
21891
|
+
* configured premium Esplora provider (`/api/lit/pending-deposits`). Requires
|
|
21892
|
+
* service mode.
|
|
21893
|
+
*/
|
|
21894
|
+
async getPendingDeposits(vaultAddress) {
|
|
21895
|
+
this.ensureInitialized();
|
|
21896
|
+
if (!isServiceModeConfig(this.config)) {
|
|
21897
|
+
throw new Error("getPendingDeposits requires service mode \u2014 standalone mode is not supported");
|
|
21898
|
+
}
|
|
21899
|
+
const trimmedAddress = vaultAddress?.trim();
|
|
21900
|
+
if (!trimmedAddress) {
|
|
21901
|
+
throw new Error("vaultAddress is required");
|
|
21902
|
+
}
|
|
21903
|
+
const serviceEndpoint = this.config.serviceEndpoint;
|
|
21904
|
+
const url = `${serviceEndpoint}/api/lit/pending-deposits?vaultAddress=${encodeURIComponent(trimmedAddress)}`;
|
|
21905
|
+
const response = await fetch(url, {
|
|
21906
|
+
headers: await this.getAuthHeader()
|
|
21907
|
+
});
|
|
21908
|
+
if (!response.ok) {
|
|
21909
|
+
throw new Error(`Pending deposits request failed: ${response.status} ${response.statusText}`);
|
|
21910
|
+
}
|
|
21911
|
+
const json = await response.json();
|
|
21912
|
+
if (!json.success)
|
|
21913
|
+
throw new Error(json.error ?? "Pending deposits request failed");
|
|
21914
|
+
return json.data ?? [];
|
|
21915
|
+
}
|
|
21025
21916
|
// ============================================================================
|
|
21026
21917
|
// HIGH-LEVEL API - Mock Token Operations (Test Networks Only)
|
|
21027
21918
|
// ============================================================================
|
|
@@ -21517,6 +22408,20 @@ Error data: ${errorData || "none"}`
|
|
|
21517
22408
|
if (this.config.debug) {
|
|
21518
22409
|
log.info(`\u{1F310} Calling liquidation server endpoint: ${endpoint}`);
|
|
21519
22410
|
}
|
|
22411
|
+
const signer = this.getSignerOrThrow();
|
|
22412
|
+
const intentSigner = await signer.getAddress();
|
|
22413
|
+
const chainId = Number(
|
|
22414
|
+
(await this.getProviderOrThrow().getNetwork()).chainId
|
|
22415
|
+
);
|
|
22416
|
+
const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
|
|
22417
|
+
const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
|
|
22418
|
+
const intentTimestamp = Math.floor(Date.now() / 1e3);
|
|
22419
|
+
const intentActionHash = keccak2565(toUtf8Bytes5("liquidate-position"));
|
|
22420
|
+
const intentHash = solidityPackedKeccak2564(
|
|
22421
|
+
["bytes32", "uint256", "uint256", "address", "bytes32"],
|
|
22422
|
+
[canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
|
|
22423
|
+
);
|
|
22424
|
+
const intentSignature = await signer.signMessage(getBytes4(intentHash));
|
|
21520
22425
|
const response = await fetch(endpoint, {
|
|
21521
22426
|
method: "POST",
|
|
21522
22427
|
headers: { "Content-Type": "application/json" },
|
|
@@ -21532,6 +22437,11 @@ Error data: ${errorData || "none"}`
|
|
|
21532
22437
|
UCDController: params.contractAddresses.ucdController,
|
|
21533
22438
|
LiquidationManagerModule: params.contractAddresses.liquidationManager
|
|
21534
22439
|
},
|
|
22440
|
+
liquidatorIntent: {
|
|
22441
|
+
timestamp: intentTimestamp,
|
|
22442
|
+
chainId,
|
|
22443
|
+
signature: intentSignature
|
|
22444
|
+
},
|
|
21535
22445
|
rpcUrl: params.rpcUrl || this.config.ethRpcUrl,
|
|
21536
22446
|
priceProviders: this.config.priceProviders,
|
|
21537
22447
|
pkpTokenId: params.pkpData.tokenId,
|