@sage-protocol/cli 0.6.5 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/sxxx.js +45 -1
- package/dist/cli/privy-auth-wallet-manager.js +54 -25
- package/dist/cli/services/governance/mode-detector.js +16 -7
- package/dist/cli/services/library/operator-mode.js +1 -1
- package/dist/cli/services/library/proposal-submit.js +63 -2
- package/dist/cli/sxxx-manager.js +113 -0
- package/dist/cli/utils/publish-dest.js +1 -1
- package/package.json +1 -1
|
@@ -479,7 +479,7 @@ function register(program) {
|
|
|
479
479
|
)
|
|
480
480
|
.addCommand(
|
|
481
481
|
new Command('delegate-self')
|
|
482
|
-
.description('Delegate voting power to yourself')
|
|
482
|
+
.description('Delegate voting power to yourself (enables you to vote)')
|
|
483
483
|
.action(async () => {
|
|
484
484
|
try {
|
|
485
485
|
const manager = new SXXXManager();
|
|
@@ -491,6 +491,50 @@ function register(program) {
|
|
|
491
491
|
}
|
|
492
492
|
})
|
|
493
493
|
)
|
|
494
|
+
.addCommand(
|
|
495
|
+
new Command('delegate')
|
|
496
|
+
.description('Delegate voting power to an address (use delegate-self to take it back)')
|
|
497
|
+
.argument('<address>', 'Address to delegate voting power to')
|
|
498
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
499
|
+
.option('--confirm', 'Confirm delegation (required without -y)')
|
|
500
|
+
.action(async (address, opts) => {
|
|
501
|
+
try {
|
|
502
|
+
const manager = new SXXXManager();
|
|
503
|
+
await manager.initialize();
|
|
504
|
+
|
|
505
|
+
// Get user's own address for comparison
|
|
506
|
+
let userAddress;
|
|
507
|
+
if (manager.signer) {
|
|
508
|
+
userAddress = await manager.signer.getAddress();
|
|
509
|
+
} else {
|
|
510
|
+
const WalletManager = require('../wallet-manager');
|
|
511
|
+
const walletManager = new WalletManager();
|
|
512
|
+
await walletManager.connect();
|
|
513
|
+
userAddress = walletManager.getAccount();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// If delegating to self, no confirmation needed
|
|
517
|
+
const isSelfDelegation = address.toLowerCase() === userAddress.toLowerCase();
|
|
518
|
+
|
|
519
|
+
if (!isSelfDelegation && !opts.yes && !opts.confirm) {
|
|
520
|
+
console.log('\n⚠️ WARNING: You are about to delegate your voting power to another address.');
|
|
521
|
+
console.log(` Delegatee: ${address}`);
|
|
522
|
+
console.log(' This means THEY will vote on your behalf, not you.\n');
|
|
523
|
+
console.log('To proceed, run one of:');
|
|
524
|
+
console.log(` sage sxxx delegate ${address} --confirm`);
|
|
525
|
+
console.log(` sage sxxx delegate ${address} -y\n`);
|
|
526
|
+
console.log('To vote yourself instead:');
|
|
527
|
+
console.log(' sage sxxx delegate-self\n');
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await manager.delegateTo(address);
|
|
532
|
+
printSuggestions('sxxx:delegate');
|
|
533
|
+
} catch (error) {
|
|
534
|
+
handleCLIError('sxxx:delegate', error);
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
)
|
|
494
538
|
.addCommand(
|
|
495
539
|
new Command('delegation')
|
|
496
540
|
.description('Check vote delegation status')
|
|
@@ -498,39 +498,68 @@ class PrivyAuthWalletManager {
|
|
|
498
498
|
.loader { border: 3px solid #f3f3f3; border-top: 3px solid #000; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 0 auto; }
|
|
499
499
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
500
500
|
#error-display { position: fixed; top: 0; left: 0; right: 0; background: #fee2e2; color: #dc2626; padding: 1rem; display: none; z-index: 1000; }
|
|
501
|
-
|
|
502
|
-
/*
|
|
501
|
+
|
|
502
|
+
/* Privy CSS variable overrides for proper modal sizing */
|
|
503
|
+
:root {
|
|
504
|
+
--privy-border-radius-sm: 4px;
|
|
505
|
+
--privy-border-radius-md: 8px;
|
|
506
|
+
--privy-border-radius-lg: 12px;
|
|
507
|
+
--privy-color-background: #ffffff;
|
|
508
|
+
--privy-color-foreground: #1a1a1a;
|
|
509
|
+
--privy-color-accent: #000000;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* Fix Privy modal height and positioning */
|
|
513
|
+
[data-privy-dialog],
|
|
514
|
+
[id*="privy-modal"],
|
|
515
|
+
[class*="privy-modal"],
|
|
516
|
+
div[role="dialog"] {
|
|
517
|
+
max-height: 90vh !important;
|
|
518
|
+
overflow-y: auto !important;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* Ensure modal content has proper height */
|
|
522
|
+
[data-privy-dialog] > div,
|
|
523
|
+
[id*="privy-modal"] > div,
|
|
524
|
+
div[role="dialog"] > div {
|
|
525
|
+
min-height: 400px !important;
|
|
526
|
+
max-height: 85vh !important;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* Fix iframe sizing issues */
|
|
530
|
+
[data-privy-dialog] iframe,
|
|
531
|
+
[id*="privy"] iframe,
|
|
532
|
+
div[role="dialog"] iframe {
|
|
533
|
+
min-height: 500px !important;
|
|
534
|
+
width: 100% !important;
|
|
535
|
+
border: none !important;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* Fix input field styling */
|
|
503
539
|
[id*="privy"] input,
|
|
504
540
|
[class*="privy"] input,
|
|
505
|
-
[class*="Privy"] input,
|
|
506
541
|
div[role="dialog"] input {
|
|
507
|
-
padding
|
|
542
|
+
padding: 12px 16px !important;
|
|
508
543
|
box-sizing: border-box !important;
|
|
544
|
+
font-size: 16px !important;
|
|
545
|
+
width: 100% !important;
|
|
509
546
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
[
|
|
513
|
-
[class*="
|
|
514
|
-
div[role="dialog"] button
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
font-size: 0 !important;
|
|
518
|
-
background: transparent !important;
|
|
519
|
-
border: none !important;
|
|
520
|
-
padding: 8px !important;
|
|
521
|
-
}
|
|
522
|
-
/* But keep the icon/arrow visible */
|
|
523
|
-
[id*="privy"] button[type="submit"] svg,
|
|
524
|
-
[class*="privy"] button[type="submit"] svg,
|
|
525
|
-
div[role="dialog"] button[type="submit"] svg {
|
|
526
|
-
width: 20px !important;
|
|
527
|
-
height: 20px !important;
|
|
547
|
+
|
|
548
|
+
/* Fix button positioning */
|
|
549
|
+
[id*="privy"] button,
|
|
550
|
+
[class*="privy"] button,
|
|
551
|
+
div[role="dialog"] button {
|
|
552
|
+
padding: 12px 24px !important;
|
|
553
|
+
font-size: 14px !important;
|
|
528
554
|
}
|
|
529
|
-
|
|
555
|
+
|
|
556
|
+
/* Ensure forms have proper layout */
|
|
530
557
|
[id*="privy"] form,
|
|
531
558
|
[class*="privy"] form,
|
|
532
559
|
div[role="dialog"] form {
|
|
533
|
-
|
|
560
|
+
display: flex !important;
|
|
561
|
+
flex-direction: column !important;
|
|
562
|
+
gap: 12px !important;
|
|
534
563
|
}
|
|
535
564
|
</style>
|
|
536
565
|
<script>
|
|
@@ -552,7 +581,7 @@ class PrivyAuthWalletManager {
|
|
|
552
581
|
"react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime",
|
|
553
582
|
"react-dom": "https://esm.sh/react-dom@18.2.0",
|
|
554
583
|
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
|
555
|
-
"@privy-io/react-auth": "https://esm.sh/@privy-io/react-auth@
|
|
584
|
+
"@privy-io/react-auth": "https://esm.sh/@privy-io/react-auth@2.4.0?external=react,react-dom"
|
|
556
585
|
}
|
|
557
586
|
}
|
|
558
587
|
</script>
|
|
@@ -447,16 +447,25 @@ class GovModeDetector {
|
|
|
447
447
|
const proposerRole = await tl.PROPOSER_ROLE();
|
|
448
448
|
const hasGovProposer = out.governor ? await tl.hasRole(proposerRole, out.governor) : false;
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
// If Governor has PROPOSER_ROLE on Timelock, route via governor-proposal
|
|
451
|
+
// This is the primary signal - governanceKind may be unknown if subdao context is missing
|
|
452
|
+
if (hasGovProposer) {
|
|
451
453
|
out.route = 'governor-proposal';
|
|
452
|
-
out.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (out.governanceKind === 'TOKEN' && !hasGovProposer) {
|
|
456
|
-
out.rationale.push('route: token governance but governor lacks PROPOSER_ROLE (misconfigured)');
|
|
454
|
+
out.governorIsProposer = true;
|
|
455
|
+
if (out.governanceKind === 'TOKEN') {
|
|
456
|
+
out.rationale.push('route: token governance + governor is proposer on timelock');
|
|
457
457
|
} else {
|
|
458
|
-
|
|
458
|
+
// Governor has PROPOSER_ROLE but governanceKind wasn't detected - infer token governance
|
|
459
|
+
out.rationale.push('route: governor has PROPOSER_ROLE on timelock (inferred token governance)');
|
|
460
|
+
if (!out.governanceKind) out.governanceKind = 'TOKEN';
|
|
459
461
|
}
|
|
462
|
+
} else if (out.governanceKind === 'OPERATOR' || !out.governanceKind) {
|
|
463
|
+
out.route = 'timelock-schedule';
|
|
464
|
+
out.rationale.push('route: schedule/execute directly via timelock (operator mode)');
|
|
465
|
+
} else {
|
|
466
|
+
// TOKEN governance but governor lacks PROPOSER_ROLE - misconfigured
|
|
467
|
+
out.route = 'timelock-schedule';
|
|
468
|
+
out.rationale.push('route: token governance but governor lacks PROPOSER_ROLE (misconfigured)');
|
|
460
469
|
}
|
|
461
470
|
return;
|
|
462
471
|
} catch (_) {
|
|
@@ -46,7 +46,7 @@ function createOperatorScheduler(overrides = {}) {
|
|
|
46
46
|
|
|
47
47
|
const timelock = new ethersLib.Contract(timelockAddress, timelockAbi, signer);
|
|
48
48
|
|
|
49
|
-
logger.log('\n⚠️ Operator mode
|
|
49
|
+
logger.log('\n⚠️ Operator mode: scheduling directly via Timelock (Governor lacks PROPOSER_ROLE or is not configured).');
|
|
50
50
|
|
|
51
51
|
const proposerRole = await timelock.PROPOSER_ROLE();
|
|
52
52
|
const hasRole = await timelock.hasRole(proposerRole, proposerAddress);
|
|
@@ -123,8 +123,69 @@ function createProposalSubmitter(overrides = {}) {
|
|
|
123
123
|
return { proposalId, transaction: tx, receipt };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
const PROPOSAL_STATES = ['Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'];
|
|
127
|
+
|
|
128
|
+
function getProposalStateName(stateNum) {
|
|
129
|
+
return PROPOSAL_STATES[Number(stateNum)] || `Unknown (${stateNum})`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function handleKnownError(error, { govAddress, proposalStake, sxxxAddress, rpcUrl, governor, targets, values, calldatas, description, subdaoAddress }) {
|
|
127
133
|
const message = (error?.message || String(error)).toLowerCase();
|
|
134
|
+
const errorData = error?.data || error?.error?.data || '';
|
|
135
|
+
|
|
136
|
+
// Detect GovernorUnexpectedProposalState (0x31b75e4d) - duplicate proposal
|
|
137
|
+
if (message.includes('0x31b75e4d') || message.includes('governorunexpectedproposalstate') || errorData.includes('0x31b75e4d')) {
|
|
138
|
+
logger.log?.('');
|
|
139
|
+
logger.log?.('❌ A proposal with the same content already exists.');
|
|
140
|
+
logger.log?.('');
|
|
141
|
+
|
|
142
|
+
// Try to compute and look up the existing proposal
|
|
143
|
+
if (governor && targets && calldatas && description) {
|
|
144
|
+
try {
|
|
145
|
+
const descHash = ethersLib.keccak256(ethersLib.toUtf8Bytes(description));
|
|
146
|
+
const proposalId = await governor.hashProposal(targets, values || [0n], calldatas, descHash);
|
|
147
|
+
const state = await governor.state(proposalId);
|
|
148
|
+
const stateName = getProposalStateName(state);
|
|
149
|
+
|
|
150
|
+
logger.log?.('📋 Existing proposal:');
|
|
151
|
+
logger.log?.(` ID: ${proposalId}`);
|
|
152
|
+
logger.log?.(` State: ${stateName}`);
|
|
153
|
+
|
|
154
|
+
// Get deadline if available
|
|
155
|
+
try {
|
|
156
|
+
const deadline = await governor.proposalDeadline(proposalId);
|
|
157
|
+
logger.log?.(` Deadline: block ${deadline}`);
|
|
158
|
+
} catch (_) { /* deadline not available */ }
|
|
159
|
+
|
|
160
|
+
logger.log?.('');
|
|
161
|
+
logger.log?.('💡 Next steps:');
|
|
162
|
+
|
|
163
|
+
const subdaoFlag = subdaoAddress ? ` --subdao ${subdaoAddress}` : '';
|
|
164
|
+
if (state === 0n || state === 1n) {
|
|
165
|
+
// Pending or Active - can vote
|
|
166
|
+
logger.log?.(` • Vote FOR: sage gov vote ${proposalId} 1${subdaoFlag}`);
|
|
167
|
+
logger.log?.(` • Check status: sage gov status ${proposalId}${subdaoFlag}`);
|
|
168
|
+
} else if (state === 5n) {
|
|
169
|
+
// Queued - ready to execute
|
|
170
|
+
logger.log?.(` • Execute: sage gov execute ${proposalId}${subdaoFlag}`);
|
|
171
|
+
} else if (state === 7n) {
|
|
172
|
+
// Already executed
|
|
173
|
+
logger.log?.(' • This proposal was already executed successfully.');
|
|
174
|
+
logger.log?.(' • If you need a new update, modify the description to create a unique proposal.');
|
|
175
|
+
} else {
|
|
176
|
+
logger.log?.(` • Check status: sage gov status ${proposalId}${subdaoFlag}`);
|
|
177
|
+
}
|
|
178
|
+
} catch (lookupErr) {
|
|
179
|
+
logger.log?.(' Could not look up existing proposal details.');
|
|
180
|
+
logger.log?.(' Use `sage proposals inbox` to find active proposals.');
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
logger.log?.('💡 Use `sage proposals inbox` to find and vote on existing proposals.');
|
|
184
|
+
}
|
|
185
|
+
logger.log?.('');
|
|
186
|
+
return { aborted: true, reason: 'duplicate-proposal' };
|
|
187
|
+
}
|
|
188
|
+
|
|
128
189
|
if (message.includes('allowance')) {
|
|
129
190
|
logger.log?.('❌ Unexpected allowance error during proposal submission');
|
|
130
191
|
logger.log?.('Please ensure you have approved SXXX for the Governor:');
|
|
@@ -157,7 +218,7 @@ function createProposalSubmitter(overrides = {}) {
|
|
|
157
218
|
try {
|
|
158
219
|
return await submitProposal(options);
|
|
159
220
|
} catch (error) {
|
|
160
|
-
const known = handleKnownError(error, options);
|
|
221
|
+
const known = await handleKnownError(error, options);
|
|
161
222
|
if (known) return known;
|
|
162
223
|
throw error;
|
|
163
224
|
}
|
package/dist/cli/sxxx-manager.js
CHANGED
|
@@ -708,6 +708,119 @@ class SXXXManager {
|
|
|
708
708
|
}
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Delegate voting power to a specific address.
|
|
713
|
+
* Use this to delegate to a representative, or delegate-self to take back your voting power.
|
|
714
|
+
* @param {string} delegatee - The address to delegate voting power to
|
|
715
|
+
*/
|
|
716
|
+
async delegateTo(delegatee) {
|
|
717
|
+
try {
|
|
718
|
+
// Validate delegatee address
|
|
719
|
+
if (!delegatee || !ethers.isAddress(delegatee)) {
|
|
720
|
+
throw new Error(`Invalid delegatee address: ${delegatee}`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
console.log(colors.blue('🗳️ Delegating votes to address...'));
|
|
724
|
+
console.log('Delegatee:', delegatee);
|
|
725
|
+
|
|
726
|
+
// Get user address - handle both Cast and regular signers
|
|
727
|
+
let userAddress;
|
|
728
|
+
if (!this.signer) {
|
|
729
|
+
const WalletManager = require('./wallet-manager');
|
|
730
|
+
const walletManager = new WalletManager();
|
|
731
|
+
await walletManager.connect();
|
|
732
|
+
userAddress = walletManager.getAccount();
|
|
733
|
+
} else {
|
|
734
|
+
userAddress = await this.signer.getAddress();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
console.log('Your address:', userAddress);
|
|
738
|
+
|
|
739
|
+
// Check if delegating to self
|
|
740
|
+
const isSelfDelegation = delegatee.toLowerCase() === userAddress.toLowerCase();
|
|
741
|
+
if (isSelfDelegation) {
|
|
742
|
+
console.log('ℹ️ Delegating to yourself (same as delegate-self)');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Use base votes token
|
|
746
|
+
const vt = this.votesToken;
|
|
747
|
+
if (!vt) {
|
|
748
|
+
console.log('⚠️ No base voting token resolved for this context.');
|
|
749
|
+
throw new Error('No base voting token resolved for delegation.');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Check current delegation
|
|
753
|
+
try {
|
|
754
|
+
const currentDelegate = await vt.delegates(userAddress);
|
|
755
|
+
console.log('Currently delegates to:', currentDelegate);
|
|
756
|
+
|
|
757
|
+
if (currentDelegate.toLowerCase() === delegatee.toLowerCase()) {
|
|
758
|
+
console.log('✅ Already delegated to this address!');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Warn if revoking self-delegation
|
|
763
|
+
if (currentDelegate.toLowerCase() === userAddress.toLowerCase() && !isSelfDelegation) {
|
|
764
|
+
console.log(colors.yellow('⚠️ You are transferring your voting power to someone else.'));
|
|
765
|
+
console.log(colors.yellow(' To take back your voting power later: sage sxxx delegate-self'));
|
|
766
|
+
}
|
|
767
|
+
} catch (e) {
|
|
768
|
+
console.log('Could not check current delegation');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Build transaction
|
|
772
|
+
const to = vt.target;
|
|
773
|
+
const data = vt.interface.encodeFunctionData('delegate', [delegatee]);
|
|
774
|
+
|
|
775
|
+
// Estimate gas
|
|
776
|
+
let gasLimit;
|
|
777
|
+
try {
|
|
778
|
+
const est = await (this.provider || vt.runner?.provider).estimateGas({ from: userAddress, to, data, value: 0 }).catch(() => null);
|
|
779
|
+
if (est) gasLimit = est + (est / 5n);
|
|
780
|
+
} catch (_) { /* non-fatal */ }
|
|
781
|
+
|
|
782
|
+
const withTimeout = (p, ms, label = 'operation') => {
|
|
783
|
+
const t = Number(ms || 15000);
|
|
784
|
+
return Promise.race([
|
|
785
|
+
p,
|
|
786
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error(`${label} timed out after ${t}ms`)), t))
|
|
787
|
+
]);
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const actionLabel = isSelfDelegation ? 'self-delegation' : `delegation to ${delegatee.slice(0, 10)}...`;
|
|
791
|
+
|
|
792
|
+
if (!this.signer) {
|
|
793
|
+
console.log(colors.yellow('🔐 Using Cast wallet for delegation transaction...'));
|
|
794
|
+
const CastWalletManager = require('./cast-wallet-manager');
|
|
795
|
+
const castWallet = new CastWalletManager();
|
|
796
|
+
await castWallet.connect();
|
|
797
|
+
const transaction = { to, data, gasLimit: gasLimit || 120000 };
|
|
798
|
+
const result = await withSpinner(`Submitting ${actionLabel}`, async () => {
|
|
799
|
+
return withTimeout(castWallet.signTransaction(transaction), process.env.SAGE_SEND_TIMEOUT_MS, 'delegate(send)');
|
|
800
|
+
}, { successText: ({ hash }) => `Delegation submitted - tx ${hash}` });
|
|
801
|
+
return result;
|
|
802
|
+
} else {
|
|
803
|
+
const sent = await withSpinner(`Submitting ${actionLabel}`, async () => {
|
|
804
|
+
const send = this.signer.sendTransaction({ to, data, gasLimit, value: 0 });
|
|
805
|
+
const tx = await withTimeout(send, process.env.SAGE_SEND_TIMEOUT_MS, 'delegate(send)');
|
|
806
|
+
const rc = await waitForReceipt(this.provider, tx, Number(process.env.SAGE_TX_WAIT_MS || 60000));
|
|
807
|
+
return { tx, receipt: rc };
|
|
808
|
+
}, { successText: ({ tx }) => `Delegation confirmed - tx ${tx.hash}` });
|
|
809
|
+
|
|
810
|
+
if (isSelfDelegation) {
|
|
811
|
+
console.log('✅ Voting power returned to you.');
|
|
812
|
+
} else {
|
|
813
|
+
console.log(`✅ Voting power delegated to ${delegatee}`);
|
|
814
|
+
console.log('💡 To take back your voting power: sage sxxx delegate-self');
|
|
815
|
+
}
|
|
816
|
+
return sent.receipt;
|
|
817
|
+
}
|
|
818
|
+
} catch (error) {
|
|
819
|
+
console.error('❌ Failed to delegate votes:', error.message);
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
711
824
|
async checkDelegation(address) {
|
|
712
825
|
try {
|
|
713
826
|
// Get target address - handle both Cast and regular signers
|
|
@@ -39,7 +39,7 @@ async function deriveDestination({ provider, subdaoOpt, workspaceSubdao, aliasRe
|
|
|
39
39
|
} else {
|
|
40
40
|
const mode = await detectGovMode({ provider, subdao: out.subdao, governor: out.governor, timelock: out.timelock });
|
|
41
41
|
// Prefer the explicit 3-axis profile when available
|
|
42
|
-
if (mode?.governanceKind === 'TOKEN') {
|
|
42
|
+
if (mode?.governanceKind === 'TOKEN' || mode?.route === 'governor-proposal') {
|
|
43
43
|
out.route = 'community';
|
|
44
44
|
out.reason = mode.route || 'token-governance';
|
|
45
45
|
} else if (mode?.playbook === 'council-closed' || mode?.route === 'safe-multisig') {
|