import { DataSource } from "@pythnetwork/xc-admin-common"; import { KeyValueConfig, Price, PriceFeed, PriceFeedContract, PrivateKey, TxResult, } from "../base"; import { Chain, StarknetChain } from "../chains"; import { Account, Contract, shortString } from "starknet"; import { ByteBuffer } from "@pythnetwork/pyth-starknet-js"; import { WormholeContract } from "./wormhole"; export class StarknetWormholeContract extends WormholeContract { static type = "StarknetWormholeContract"; getId(): string { return `${this.chain.getId()}_${this.address}`; } getType(): string { return StarknetWormholeContract.type; } toJson() { return { chain: this.chain.getId(), address: this.address, type: StarknetWormholeContract.type, }; } static fromJson( chain: Chain, parsed: { type: string; address: string; } ): StarknetWormholeContract { if (parsed.type !== StarknetWormholeContract.type) throw new Error("Invalid type"); if (!(chain instanceof StarknetChain)) throw new Error(`Wrong chain type ${chain}`); return new StarknetWormholeContract(chain, parsed.address); } constructor(public chain: StarknetChain, public address: string) { super(); } async getContractClient(): Promise { const provider = this.chain.getProvider(); const classData = await provider.getClassAt(this.address); return new Contract(classData.abi, this.address, provider); } async getCurrentGuardianSetIndex(): Promise { const contract = await this.getContractClient(); return Number(await contract.get_current_guardian_set_index()); } async getChainId(): Promise { const contract = await this.getContractClient(); return Number(await contract.chain_id()); } async getGuardianSet(): Promise { const contract = await this.getContractClient(); const setIndex = await contract.get_current_guardian_set_index(); const set = await contract.get_guardian_set(setIndex); return set.keys.map((key: bigint) => key.toString(16).padStart(40, "0")); } async upgradeGuardianSets( senderPrivateKey: PrivateKey, vaa: Buffer ): Promise { const senderAddress = await this.chain.getAccountAddress(senderPrivateKey); const provider = this.chain.getProvider(); const contract = await this.getContractClient(); const account = new Account( provider, "0x" + senderAddress, "0x" + senderPrivateKey ); contract.connect(account); const updateData = ByteBuffer.fromBuffer(vaa); const tx = await contract.submit_new_guardian_set(updateData); const info = await provider.waitForTransaction(tx.transaction_hash); return { id: tx.transaction_hash, info }; } } export class StarknetPriceFeedContract extends PriceFeedContract { static type = "StarknetPriceFeedContract"; constructor(public chain: StarknetChain, public address: string) { super(); } static fromJson( chain: Chain, parsed: { type: string; address: string; } ): StarknetPriceFeedContract { if (parsed.type !== StarknetPriceFeedContract.type) throw new Error("Invalid type"); if (!(chain instanceof StarknetChain)) throw new Error(`Wrong chain type ${chain}`); return new StarknetPriceFeedContract(chain, parsed.address); } toJson(): KeyValueConfig { return { chain: this.chain.getId(), address: this.address, type: StarknetPriceFeedContract.type, }; } // Not implemented in the Starknet contract. getValidTimePeriod(): Promise { throw new Error("Unsupported"); } getChain(): StarknetChain { return this.chain; } async getContractClient(): Promise { const provider = this.chain.getProvider(); const classData = await provider.getClassAt(this.address); return new Contract(classData.abi, this.address, provider); } async getDataSources(): Promise { const contract = await this.getContractClient(); const sources: { emitter_chain_id: bigint; emitter_address: bigint }[] = await contract.valid_data_sources(); return sources.map((source) => { return { emitterChain: Number(source.emitter_chain_id), emitterAddress: source.emitter_address.toString(16).padStart(64, "0"), }; }); } async getBaseUpdateFee(): Promise<{ amount: string; denom?: string | undefined; }> { const tokens = await this.getFeeTokenAddresses(); return await this.getBaseUpdateFeeInToken(tokens[0]); } /** * Returns the list of accepted fee tokens. * @returns hex encoded token addresses without 0x prefix */ async getFeeTokenAddresses(): Promise { const contract = await this.getContractClient(); const tokens: bigint[] = await contract.fee_token_addresses(); return tokens.map((t) => t.toString(16).padStart(64, "0")); } /** * Returns the single update fee and symbol of the specified token. * @param token hex encoded token address without 0x prefix */ async getBaseUpdateFeeInToken( token: string ): Promise<{ amount: string; denom?: string | undefined }> { token = "0x" + token; const provider = this.chain.getProvider(); const contract = await this.getContractClient(); const fee: bigint = await contract.get_single_update_fee(token); const tokenClassData = await provider.getClassAt(token); const tokenContract = new Contract(tokenClassData.abi, token, provider); const denom = shortString.decodeShortString(await tokenContract.symbol()); return { amount: fee.toString(), denom }; } async getLastExecutedGovernanceSequence(): Promise { const contract = await this.getContractClient(); return Number(await contract.last_executed_governance_sequence()); } async getPriceFeed(feedId: string): Promise { const contract = await this.getContractClient(); const result = await contract.query_price_feed_unsafe("0x" + feedId); if (result.Ok !== undefined) { return { price: convertPrice(result.Ok.price), emaPrice: convertPrice(result.Ok.ema_price), }; } else { throw new Error(JSON.stringify(result.Err)); } } async executeUpdatePriceFeed( senderPrivateKey: PrivateKey, vaas: Buffer[] ): Promise { const senderAddress = await this.chain.getAccountAddress(senderPrivateKey); const provider = this.chain.getProvider(); const contract = await this.getContractClient(); const account = new Account( provider, "0x" + senderAddress, "0x" + senderPrivateKey ); contract.connect(account); const feeToken = "0x" + (await this.getFeeTokenAddresses())[0]; const tokenClassData = await provider.getClassAt(feeToken); const tokenContract = new Contract(tokenClassData.abi, feeToken, provider); tokenContract.connect(account); const ids = []; const infos = []; for (const vaa of vaas) { const updateData = ByteBuffer.fromBuffer(vaa); const feeAmount = await contract.get_update_fee(updateData, feeToken); const feeTx = await tokenContract.approve(this.address, feeAmount); await provider.waitForTransaction(feeTx.transaction_hash); const tx = await contract.update_price_feeds(updateData); const info = await provider.waitForTransaction(tx.transaction_hash); ids.push(tx.transaction_hash); infos.push(info); } return { id: ids.join(","), info: infos }; } async executeGovernanceInstruction( senderPrivateKey: PrivateKey, vaa: Buffer ): Promise { const senderAddress = await this.chain.getAccountAddress(senderPrivateKey); const provider = this.chain.getProvider(); const contract = await this.getContractClient(); const account = new Account( provider, "0x" + senderAddress, "0x" + senderPrivateKey ); contract.connect(account); const updateData = ByteBuffer.fromBuffer(vaa); const tx = await contract.execute_governance_instruction(updateData); const info = await provider.waitForTransaction(tx.transaction_hash); return { id: tx.transaction_hash, info }; } async getGovernanceDataSource(): Promise { const contract = await this.getContractClient(); const source: { emitter_chain_id: bigint; emitter_address: bigint } = await contract.governance_data_source(); return { emitterChain: Number(source.emitter_chain_id), emitterAddress: source.emitter_address.toString(16).padStart(64, "0"), }; } getId(): string { return `${this.chain.getId()}_${this.address}`; } getType(): string { return StarknetPriceFeedContract.type; } } function convertPrice(obj: { price: bigint; conf: bigint; expo: bigint; publish_time: bigint; }): Price { return { price: obj.price.toString(), conf: obj.conf.toString(), expo: obj.expo.toString(), publishTime: obj.publish_time.toString(), }; }