diff --git a/frontend/src/services/ContractUpgradeService.ts b/frontend/src/services/ContractUpgradeService.ts index edfcc59f..66d348a7 100644 --- a/frontend/src/services/ContractUpgradeService.ts +++ b/frontend/src/services/ContractUpgradeService.ts @@ -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; @@ -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 { @@ -88,18 +98,268 @@ export class ContractUpgradeService { } async getImplementation(): Promise { - 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 { - 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 { - 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 { - 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 { + 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 { + 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 { + const pending = await this.getPendingUpgrade(); + return pending !== null; + } + + async canExecuteUpgrade(): Promise { + 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 { + 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 { + const isValid = await this.validateImplementationAddress(address); + if (!isValid) { + return false; + } + + const current = await this.getImplementation(); + return address !== current; + } + + async getLastUpgradeRecord(): Promise { + 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; } } diff --git a/frontend/src/services/MigrationService.ts b/frontend/src/services/MigrationService.ts index 04e5e5ce..238f5839 100644 --- a/frontend/src/services/MigrationService.ts +++ b/frontend/src/services/MigrationService.ts @@ -1,5 +1,13 @@ import { openContractCall } from '@stacks/connect'; -import { uintCV, stringUtf8CV, bufferCV, boolCV, PostConditionMode } from '@stacks/transactions'; +import { + uintCV, + stringUtf8CV, + bufferCV, + boolCV, + PostConditionMode, + callReadOnlyFunction, + cvToValue, +} from '@stacks/transactions'; export interface Migration { migrationId: number; @@ -18,9 +26,11 @@ export interface MigrationData { export class MigrationService { private migrationContract: { address: string; name: string }; + private network: any; - constructor(migrationContract: { address: string; name: string }) { + constructor(migrationContract: { address: string; name: string }, network?: any) { this.migrationContract = migrationContract; + this.network = network; } async registerMigration( @@ -89,22 +99,141 @@ export class MigrationService { } async getCurrentVersion(): Promise { - return 1; + try { + const contractAddress = this.extractAddress(this.migrationContract.address); + const response = await callReadOnlyFunction({ + contractAddress, + contractName: this.migrationContract.name, + functionName: 'get-current-version', + functionArgs: [], + network: this.network, + senderAddress: contractAddress, + }); + + if (response.ok && typeof response.value === 'object') { + const value = cvToValue(response.value); + return typeof value === 'number' ? value : 1; + } + return 1; + } catch (error) { + console.error('Failed to get current version:', error); + return 1; + } + } + + private extractAddress(addressInput: string): string { + if (addressInput.includes('.')) { + return addressInput.split('.')[0]; + } + return addressInput; } async getMigration(migrationId: number): Promise { - return null; + try { + const contractAddress = this.extractAddress(this.migrationContract.address); + const response = await callReadOnlyFunction({ + contractAddress, + contractName: this.migrationContract.name, + functionName: 'get-migration', + functionArgs: [uintCV(migrationId)], + network: this.network, + senderAddress: contractAddress, + }); + + if (response.ok && response.value) { + const result = cvToValue(response.value); + if (result && typeof result === 'object') { + const migration = result as any; + return { + migrationId, + version: migration.version || 0, + description: migration.description || '', + executed: migration.executed || false, + executedAt: migration['executed-at'] || undefined, + executedBy: migration['executed-by'] || undefined, + rollbackAvailable: migration['rollback-available'] || false, + }; + } + } + return null; + } catch (error) { + console.error(`Failed to get migration ${migrationId}:`, error); + return null; + } } async getMigrationData(migrationId: number): Promise { - return null; + try { + const contractAddress = this.extractAddress(this.migrationContract.address); + const response = await callReadOnlyFunction({ + contractAddress, + contractName: this.migrationContract.name, + functionName: 'get-migration-data', + functionArgs: [uintCV(migrationId)], + network: this.network, + senderAddress: contractAddress, + }); + + if (response.ok && response.value) { + const result = cvToValue(response.value); + if (result && typeof result === 'object') { + const data = result as any; + return { + dataHash: data['data-hash'] || new Uint8Array(), + dataSize: data['data-size'] || 0, + }; + } + } + return null; + } catch (error) { + console.error(`Failed to get migration data ${migrationId}:`, error); + return null; + } } async isMigrationExecuted(migrationId: number): Promise { - return false; + try { + const contractAddress = this.extractAddress(this.migrationContract.address); + const response = await callReadOnlyFunction({ + contractAddress, + contractName: this.migrationContract.name, + functionName: 'is-migration-executed', + functionArgs: [uintCV(migrationId)], + network: this.network, + senderAddress: contractAddress, + }); + + if (response.ok && typeof response.value === 'object') { + const value = cvToValue(response.value); + return value === true; + } + return false; + } catch (error) { + console.error(`Failed to check migration execution ${migrationId}:`, error); + return false; + } } async getMigrationCount(): Promise { - return 0; + try { + const contractAddress = this.extractAddress(this.migrationContract.address); + const response = await callReadOnlyFunction({ + contractAddress, + contractName: this.migrationContract.name, + functionName: 'get-migration-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 migration count:', error); + return 0; + } } }