diff --git a/contract_manager/package.json b/contract_manager/package.json index c8d0fe1c24..b12975471d 100644 --- a/contract_manager/package.json +++ b/contract_manager/package.json @@ -32,8 +32,8 @@ "@pythnetwork/cosmwasm-deploy-tools": "workspace:*", "@pythnetwork/entropy-sdk-solidity": "workspace:*", "@pythnetwork/price-service-client": "workspace:*", - "@pythnetwork/pyth-sdk-solidity": "workspace:^", "@pythnetwork/pyth-fuel-js": "workspace:*", + "@pythnetwork/pyth-sdk-solidity": "workspace:^", "@pythnetwork/pyth-sui-js": "workspace:*", "@pythnetwork/solana-utils": "workspace:^", "@pythnetwork/xc-admin-common": "workspace:*", @@ -45,6 +45,7 @@ "extract-files": "^13.0.0", "fuels": "^0.89.2", "ramda": "^0.30.1", + "starknet": "^5.24.3", "ts-node": "^10.9.1", "typescript": "^5.3.3", "web3": "^1.8.2", diff --git a/contract_manager/src/chains.ts b/contract_manager/src/chains.ts index e38b3196c2..2196414771 100644 --- a/contract_manager/src/chains.ts +++ b/contract_manager/src/chains.ts @@ -24,6 +24,7 @@ import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519"; import { TokenId } from "./token"; import { BN, Provider, Wallet, WalletUnlocked } from "fuels"; import { FUEL_ETH_ASSET_ID } from "@pythnetwork/pyth-fuel-js"; +import { RpcProvider } from "starknet"; export type ChainConfig = Record & { mainnet: boolean; @@ -630,3 +631,62 @@ export class FuelChain extends Chain { return Number(balance) / 10 ** 9; } } + +export class StarknetChain extends Chain { + static type = "StarknetChain"; + + constructor( + id: string, + mainnet: boolean, + wormholeChainName: string, + public rpcUrl: string + ) { + super(id, mainnet, wormholeChainName, undefined); + } + + getType(): string { + return StarknetChain.type; + } + + toJson(): KeyValueConfig { + return { + id: this.id, + wormholeChainName: this.wormholeChainName, + mainnet: this.mainnet, + rpcUrl: this.rpcUrl, + type: StarknetChain.type, + }; + } + + static fromJson(parsed: ChainConfig): StarknetChain { + if (parsed.type !== StarknetChain.type) throw new Error("Invalid type"); + return new StarknetChain( + parsed.id, + parsed.mainnet, + parsed.wormholeChainName, + parsed.rpcUrl + ); + } + + /** + * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain + * @param digest hex string of the felt252 class hash of the new contract class extended to uint256 in BE + */ + generateGovernanceUpgradePayload(digest: string): Buffer { + return new UpgradeContract256Bit(this.wormholeChainName, digest).encode(); + } + + // Account address derivation on Starknet depends + // on the wallet application and constructor arguments used. + async getAccountAddress(privateKey: PrivateKey): Promise { + throw new Error("Unsupported"); + } + + async getAccountBalance(privateKey: PrivateKey): Promise { + throw new Error("Unsupported"); + } + + getProvider(): RpcProvider { + return new RpcProvider({ nodeUrl: this.rpcUrl }); + } +} diff --git a/contract_manager/src/contracts/starknet.ts b/contract_manager/src/contracts/starknet.ts new file mode 100644 index 0000000000..73e8629ed3 --- /dev/null +++ b/contract_manager/src/contracts/starknet.ts @@ -0,0 +1,100 @@ +import { DataSource } from "@pythnetwork/xc-admin-common"; +import { + KeyValueConfig, + PriceFeed, + PriceFeedContract, + PrivateKey, + TxResult, +} from "../base"; +import { Chain, StarknetChain } from "../chains"; +import { Contract } from "starknet"; + +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), + }; + }); + } + + getBaseUpdateFee(): Promise<{ amount: string; denom?: string | undefined }> { + throw new Error("Method not implemented."); + } + getLastExecutedGovernanceSequence(): Promise { + throw new Error("Method not implemented."); + } + getPriceFeed(feedId: string): Promise { + throw new Error("Method not implemented."); + } + executeUpdatePriceFeed( + senderPrivateKey: PrivateKey, + vaas: Buffer[] + ): Promise { + throw new Error("Method not implemented."); + } + executeGovernanceInstruction( + senderPrivateKey: PrivateKey, + vaa: Buffer + ): Promise { + throw new Error("Method not implemented."); + } + getGovernanceDataSource(): Promise { + throw new Error("Method not implemented."); + } + + getId(): string { + return `${this.chain.getId()}_${this.address}`; + } + + getType(): string { + return StarknetPriceFeedContract.type; + } +} diff --git a/contract_manager/src/shell.ts b/contract_manager/src/shell.ts index e609fb0e1e..1462552495 100644 --- a/contract_manager/src/shell.ts +++ b/contract_manager/src/shell.ts @@ -6,11 +6,12 @@ repl.setService(service); repl.start(); repl.evalCode( "import { loadHotWallet, Vault } from './src/governance';" + - "import { SuiChain, CosmWasmChain, AptosChain, EvmChain } from './src/chains';" + + "import { SuiChain, CosmWasmChain, AptosChain, EvmChain, StarknetChain } from './src/chains';" + "import { SuiPriceFeedContract } from './src/contracts/sui';" + "import { CosmWasmWormholeContract, CosmWasmPriceFeedContract } from './src/contracts/cosmwasm';" + "import { EvmWormholeContract, EvmPriceFeedContract } from './src/contracts/evm';" + "import { AptosWormholeContract, AptosPriceFeedContract } from './src/contracts/aptos';" + + "import { StarknetPriceFeedContract } from './src/contracts/starknet';" + "import { DefaultStore } from './src/store';" + "import { toPrivateKey } from './src/base';" + "DefaultStore" diff --git a/contract_manager/src/store.ts b/contract_manager/src/store.ts index ea7078af12..8dc293b07e 100644 --- a/contract_manager/src/store.ts +++ b/contract_manager/src/store.ts @@ -2,6 +2,7 @@ import { AptosChain, Chain, CosmWasmChain, + StarknetChain, EvmChain, FuelChain, GlobalChain, @@ -26,6 +27,7 @@ import { PriceFeedContract, Storable } from "./base"; import { parse, stringify } from "yaml"; import { readdirSync, readFileSync, statSync, writeFileSync } from "fs"; import { Vault } from "./governance"; +import { StarknetPriceFeedContract } from "./contracts/starknet"; export class Store { public chains: Record = { global: new GlobalChain() }; @@ -73,6 +75,7 @@ export class Store { [EvmChain.type]: EvmChain, [AptosChain.type]: AptosChain, [FuelChain.type]: FuelChain, + [StarknetChain.type]: StarknetChain, }; this.getYamlFiles(`${this.path}/chains/`).forEach((yamlFile) => { @@ -135,6 +138,7 @@ export class Store { [EvmWormholeContract.type]: EvmWormholeContract, [FuelPriceFeedContract.type]: FuelPriceFeedContract, [FuelWormholeContract.type]: FuelWormholeContract, + [StarknetPriceFeedContract.type]: StarknetPriceFeedContract, }; this.getYamlFiles(`${this.path}/contracts/`).forEach((yamlFile) => { const parsedArray = parse(readFileSync(yamlFile, "utf-8")); diff --git a/contract_manager/store/chains/StarknetChains.yaml b/contract_manager/store/chains/StarknetChains.yaml new file mode 100644 index 0000000000..0d3d4e5084 --- /dev/null +++ b/contract_manager/store/chains/StarknetChains.yaml @@ -0,0 +1,10 @@ +- id: starknet_sepolia + wormholeChainName: starknet_sepolia + mainnet: false + rpcUrl: https://starknet-sepolia.public.blastapi.io/ + type: StarknetChain +- id: starknet_mainnet + wormholeChainName: starknet + mainnet: true + rpcUrl: https://starknet-mainnet.public.blastapi.io/ + type: StarknetChain diff --git a/contract_manager/store/contracts/StarknetPriceFeedContracts.yaml b/contract_manager/store/contracts/StarknetPriceFeedContracts.yaml new file mode 100644 index 0000000000..8cebcbcd4b --- /dev/null +++ b/contract_manager/store/contracts/StarknetPriceFeedContracts.yaml @@ -0,0 +1,3 @@ +- chain: starknet_sepolia + type: StarknetPriceFeedContract + address: "0x07f2b07b6b5365e7ee055bda4c0ecabd867e6d3ee298d73aea32b027667186d6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b5b73e8f8..35e347ef19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -386,6 +386,9 @@ importers: ramda: specifier: ^0.30.1 version: 0.30.1 + starknet: + specifier: ^5.24.3 + version: 5.24.3(encoding@0.1.13) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@20.14.2)(typescript@5.4.5) @@ -5502,6 +5505,9 @@ packages: '@scure/bip39@1.2.2': resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} + '@scure/starknet@0.3.0': + resolution: {integrity: sha512-Ma66yZlwa5z00qI5alSxdWtIpky5LBhy22acVFdoC5kwwbd9uDyMWEYzWHdNyKmQg9t5Y2UOXzINMeb3yez+Gw==} + '@sentry/core@5.30.0': resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} engines: {node: '>=6'} @@ -11526,6 +11532,9 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic-fetch@3.0.0: + resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} + isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} @@ -12391,6 +12400,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lossless-json@2.0.11: + resolution: {integrity: sha512-BP0vn+NGYvzDielvBZaFain/wgeJ1hTvURCqtKvhr1SCPePdaaTanmmcplrHfEJSJOUql7hk4FHwToNJjWRY3g==} + loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} deprecated: Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5 @@ -15238,6 +15250,9 @@ packages: stat-mode@0.3.0: resolution: {integrity: sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng==} + starknet@5.24.3: + resolution: {integrity: sha512-v0TuaNc9iNtHdbIRzX372jfQH1vgx2rwBHQDMqK4DqjJbwFEE5dog8Go6rGiZVW750NqRSWrZ7ahqyRNc3bscg==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -16272,6 +16287,9 @@ packages: urijs@1.19.11: resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} @@ -24957,6 +24975,11 @@ snapshots: '@noble/hashes': 1.3.3 '@scure/base': 1.1.6 + '@scure/starknet@0.3.0': + dependencies: + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.3 + '@sentry/core@5.30.0': dependencies: '@sentry/hub': 5.30.0 @@ -33518,6 +33541,13 @@ snapshots: isobject@3.0.1: {} + isomorphic-fetch@3.0.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + whatwg-fetch: 3.6.20 + transitivePeerDependencies: + - encoding + isomorphic-unfetch@3.1.0(encoding@0.1.13): dependencies: node-fetch: 2.7.0(encoding@0.1.13) @@ -35587,6 +35617,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lossless-json@2.0.11: {} + loupe@2.3.6: dependencies: get-func-name: 2.0.0 @@ -39374,6 +39406,18 @@ snapshots: stat-mode@0.3.0: {} + starknet@5.24.3(encoding@0.1.13): + dependencies: + '@noble/curves': 1.2.0 + '@scure/base': 1.1.6 + '@scure/starknet': 0.3.0 + isomorphic-fetch: 3.0.0(encoding@0.1.13) + lossless-json: 2.0.11 + pako: 2.1.0 + url-join: 4.0.1 + transitivePeerDependencies: + - encoding + statuses@1.5.0: {} statuses@2.0.1: {} @@ -40815,6 +40859,8 @@ snapshots: urijs@1.19.11: {} + url-join@4.0.1: {} + url-parse@1.5.10: dependencies: querystringify: 2.2.0