Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a950c71
chore: add required imports for contract read-only function calls
Mosas2000 May 30, 2026
b3288ea
refactor: add network parameter to MigrationService constructor
Mosas2000 May 30, 2026
88175e4
refactor: add helper method for contract address parsing
Mosas2000 May 30, 2026
9e79a91
feat: implement getCurrentVersion method with contract call
Mosas2000 May 30, 2026
177e6dd
feat: implement getMigration method to query migration details
Mosas2000 May 30, 2026
9c268a5
feat: implement getMigrationData method for data access
Mosas2000 May 30, 2026
4dd5618
feat: implement isMigrationExecuted method for status checking
Mosas2000 May 30, 2026
ca60beb
feat: implement getMigrationCount method to query total migrations
Mosas2000 May 30, 2026
a1404ec
chore: add required imports for contract read-only function calls
Mosas2000 May 31, 2026
69c6805
refactor: add network parameter to ContractUpgradeService constructor
Mosas2000 May 31, 2026
63b0964
refactor: add helper method for contract address parsing
Mosas2000 May 31, 2026
6770082
feat: implement getImplementation method to query current implementation
Mosas2000 May 31, 2026
2212e12
feat: implement getPendingUpgrade method to query pending implementation
Mosas2000 May 31, 2026
764f704
feat: implement getUpgradeHistory method to retrieve upgrade records
Mosas2000 May 31, 2026
1d78d6a
feat: implement getUpgradeCount method to query total upgrades
Mosas2000 May 31, 2026
3952706
feat: implement getUpgradeTimelock method to query timelock blocks
Mosas2000 May 31, 2026
912047a
feat: add isUpgradePending utility method for status checks
Mosas2000 May 31, 2026
d7f75c5
feat: add canExecuteUpgrade method for upgrade readiness check
Mosas2000 May 31, 2026
31ee7a4
feat: add getUpgradeHistoryBatch method for batch retrieval
Mosas2000 May 31, 2026
9fd5a15
feat: add validateImplementationAddress method for address validation
Mosas2000 May 31, 2026
f5a65b3
feat: add compareImplementations method for upgrade diff analysis
Mosas2000 May 31, 2026
979ea93
feat: add getUpgradeMetadata method for comprehensive status snapshot
Mosas2000 May 31, 2026
ae8c922
feat: add isImplementationValid method for proposal validation
Mosas2000 May 31, 2026
796848f
feat: add getLastUpgradeRecord method for recent upgrade tracking
Mosas2000 May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 266 additions & 6 deletions frontend/src/services/ContractUpgradeService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { openContractCall } from '@stacks/connect';
import { uintCV, principalCV, bufferCV, stringUtf8CV, PostConditionMode } from '@stacks/transactions';
import {
uintCV,
principalCV,
bufferCV,
stringUtf8CV,
PostConditionMode,
callReadOnlyFunction,
cvToValue,
} from '@stacks/transactions';

export interface UpgradeProposal {
newImplementation: string;
Expand All @@ -18,9 +26,11 @@ export interface UpgradeHistory {

export class ContractUpgradeService {
private proxyContract: { address: string; name: string };
private network: any;

constructor(proxyContract: { address: string; name: string }) {
constructor(proxyContract: { address: string; name: string }, network?: any) {
this.proxyContract = proxyContract;
this.network = network;
}

async proposeUpgrade(newImplementation: string, userAddress: string): Promise<void> {
Expand Down Expand Up @@ -88,18 +98,268 @@ export class ContractUpgradeService {
}

async getImplementation(): Promise<string | null> {
return null;
try {
const contractAddress = this.extractAddress(this.proxyContract.address);
const response = await callReadOnlyFunction({
contractAddress,
contractName: this.proxyContract.name,
functionName: 'get-implementation',
functionArgs: [],
network: this.network,
senderAddress: contractAddress,
});

if (response.ok && response.value) {
const value = cvToValue(response.value);
return typeof value === 'string' ? value : null;
}
return null;
} catch (error) {
console.error('Failed to get implementation:', error);
return null;
}
}

private extractAddress(addressInput: string): string {
if (addressInput.includes('.')) {
return addressInput.split('.')[0];
}
return addressInput;
}

async getPendingUpgrade(): Promise<UpgradeProposal | null> {
return null;
try {
const contractAddress = this.extractAddress(this.proxyContract.address);
const response = await callReadOnlyFunction({
contractAddress,
contractName: this.proxyContract.name,
functionName: 'get-pending-implementation',
functionArgs: [],
network: this.network,
senderAddress: contractAddress,
});

if (response.ok && response.value) {
const result = cvToValue(response.value);
if (result && typeof result === 'object') {
return {
newImplementation: result as string,
proposedAt: 0,
proposedBy: '',
timelockExpires: 0,
};
}
if (result === null) {
return null;
}
}
return null;
} catch (error) {
console.error('Failed to get pending upgrade:', error);
return null;
}
}

async getUpgradeHistory(upgradeId: number): Promise<UpgradeHistory | null> {
return null;
try {
const contractAddress = this.extractAddress(this.proxyContract.address);
const response = await callReadOnlyFunction({
contractAddress,
contractName: this.proxyContract.name,
functionName: 'get-upgrade-history',
functionArgs: [uintCV(upgradeId)],
network: this.network,
senderAddress: contractAddress,
});

if (response.ok && response.value) {
const result = cvToValue(response.value);
if (result && typeof result === 'object') {
const history = result as any;
return {
upgradeId,
fromImplementation: history['from-implementation'] || '',
toImplementation: history['to-implementation'] || '',
upgradedAt: history['upgraded-at'] || 0,
upgradedBy: history['upgraded-by'] || '',
};
}
}
return null;
} catch (error) {
console.error(`Failed to get upgrade history ${upgradeId}:`, error);
return null;
}
}

async getUpgradeCount(): Promise<number> {
return 0;
try {
const contractAddress = this.extractAddress(this.proxyContract.address);
const response = await callReadOnlyFunction({
contractAddress,
contractName: this.proxyContract.name,
functionName: 'get-upgrade-count',
functionArgs: [],
network: this.network,
senderAddress: contractAddress,
});

if (response.ok && typeof response.value === 'object') {
const value = cvToValue(response.value);
return typeof value === 'number' ? value : 0;
}
return 0;
} catch (error) {
console.error('Failed to get upgrade count:', error);
return 0;
}
}

async getUpgradeTimelock(): Promise<number | null> {
try {
const contractAddress = this.extractAddress(this.proxyContract.address);
const response = await callReadOnlyFunction({
contractAddress,
contractName: this.proxyContract.name,
functionName: 'get-upgrade-timelock',
functionArgs: [],
network: this.network,
senderAddress: contractAddress,
});

if (response.ok && typeof response.value === 'object') {
const value = cvToValue(response.value);
return typeof value === 'number' ? value : null;
}
return null;
} catch (error) {
console.error('Failed to get upgrade timelock:', error);
return null;
}
}

async getOwner(): Promise<string | null> {
try {
const contractAddress = this.extractAddress(this.proxyContract.address);
const response = await callReadOnlyFunction({
contractAddress,
contractName: this.proxyContract.name,
functionName: 'get-owner',
functionArgs: [],
network: this.network,
senderAddress: contractAddress,
});

if (response.ok && response.value) {
const value = cvToValue(response.value);
return typeof value === 'string' ? value : null;
}
return null;
} catch (error) {
console.error('Failed to get owner:', error);
return null;
}
}

async isUpgradePending(): Promise<boolean> {
const pending = await this.getPendingUpgrade();
return pending !== null;
}

async canExecuteUpgrade(): Promise<boolean> {
const pending = await this.getPendingUpgrade();
if (!pending) {
return false;
}

const timelock = await this.getUpgradeTimelock();
if (timelock === null) {
return false;
}

return true;
}

async getUpgradeHistoryBatch(startId: number, count: number): Promise<(UpgradeHistory | null)[]> {
const results: (UpgradeHistory | null)[] = [];
for (let i = startId; i < startId + count; i++) {
const history = await this.getUpgradeHistory(i);
results.push(history);
}
return results;
}

async validateImplementationAddress(address: string): Promise<boolean> {
if (!address || typeof address !== 'string') {
return false;
}

const principalRegex = /^(ST|SM)[A-Z0-9]+$/i;
return principalRegex.test(address);
}

async compareImplementations(): Promise<{
current: string | null;
pending: UpgradeProposal | null;
same: boolean;
}> {
const current = await this.getImplementation();
const pending = await this.getPendingUpgrade();

return {
current,
pending,
same: current === pending?.newImplementation,
};
}

async getUpgradeMetadata(): Promise<{
owner: string | null;
current: string | null;
pending: UpgradeProposal | null;
timelock: number | null;
count: number;
}> {
const [owner, current, pending, timelock, count] = await Promise.all([
this.getOwner(),
this.getImplementation(),
this.getPendingUpgrade(),
this.getUpgradeTimelock(),
this.getUpgradeCount(),
]);

return {
owner,
current,
pending,
timelock,
count,
};
}

async isImplementationValid(address: string): Promise<boolean> {
const isValid = await this.validateImplementationAddress(address);
if (!isValid) {
return false;
}

const current = await this.getImplementation();
return address !== current;
}

async getLastUpgradeRecord(): Promise<UpgradeHistory | null> {
const count = await this.getUpgradeCount();
if (count === 0) {
return null;
}

return this.getUpgradeHistory(count - 1);
}

private extractAddress(addressInput: string): string {
if (addressInput.includes('.')) {
return addressInput.split('.')[0];
}
return addressInput;
}
}
Loading
Loading