Переглянути джерело

feat(contract_manager): add starknet contract (#1715)

Pavel Strakhov 1 рік тому
батько
коміт
29c5e0f1f7

+ 2 - 1
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",

+ 60 - 0
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<string, string> & {
   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<string> {
+    throw new Error("Unsupported");
+  }
+
+  async getAccountBalance(privateKey: PrivateKey): Promise<number> {
+    throw new Error("Unsupported");
+  }
+
+  getProvider(): RpcProvider {
+    return new RpcProvider({ nodeUrl: this.rpcUrl });
+  }
+}

+ 100 - 0
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<number> {
+    throw new Error("Unsupported");
+  }
+
+  getChain(): StarknetChain {
+    return this.chain;
+  }
+
+  async getContractClient(): Promise<Contract> {
+    const provider = this.chain.getProvider();
+    const classData = await provider.getClassAt(this.address);
+    return new Contract(classData.abi, this.address, provider);
+  }
+
+  async getDataSources(): Promise<DataSource[]> {
+    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<number> {
+    throw new Error("Method not implemented.");
+  }
+  getPriceFeed(feedId: string): Promise<PriceFeed | undefined> {
+    throw new Error("Method not implemented.");
+  }
+  executeUpdatePriceFeed(
+    senderPrivateKey: PrivateKey,
+    vaas: Buffer[]
+  ): Promise<TxResult> {
+    throw new Error("Method not implemented.");
+  }
+  executeGovernanceInstruction(
+    senderPrivateKey: PrivateKey,
+    vaa: Buffer
+  ): Promise<TxResult> {
+    throw new Error("Method not implemented.");
+  }
+  getGovernanceDataSource(): Promise<DataSource> {
+    throw new Error("Method not implemented.");
+  }
+
+  getId(): string {
+    return `${this.chain.getId()}_${this.address}`;
+  }
+
+  getType(): string {
+    return StarknetPriceFeedContract.type;
+  }
+}

+ 2 - 1
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"

+ 4 - 0
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<string, Chain> = { 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"));

+ 10 - 0
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

+ 3 - 0
contract_manager/store/contracts/StarknetPriceFeedContracts.yaml

@@ -0,0 +1,3 @@
+- chain: starknet_sepolia
+  type: StarknetPriceFeedContract
+  address: "0x07f2b07b6b5365e7ee055bda4c0ecabd867e6d3ee298d73aea32b027667186d6"

+ 46 - 0
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