Răsfoiți Sursa

feat(contract_manager): add near chains and contracts (#2331)

* wip

* feat(contract_manager): near chain support (wip)

* feat(contract_manager): add near contract support (wip)

* feat(contract_manager): support near wormhole contract and upgrade payload

* feat(contract_manager): use real near wormhole chain id

* chore: fix ci

* feat(contract_manager): store near rpc url and network id in yaml

* chore: fix typo

* chore: move near wormhole id to a separate section with comment
Pavel Strakhov 9 luni în urmă
părinte
comite
08ceaa7b04

+ 2 - 1
contract_manager/package.json

@@ -36,8 +36,8 @@
     "@pythnetwork/pyth-sdk-solidity": "workspace:^",
     "@pythnetwork/pyth-starknet-js": "^0.2.1",
     "@pythnetwork/pyth-sui-js": "workspace:*",
-    "@pythnetwork/pyth-ton-js": "workspace:*",
     "@pythnetwork/pyth-ton": "workspace:*",
+    "@pythnetwork/pyth-ton-js": "workspace:*",
     "@pythnetwork/solana-utils": "workspace:^",
     "@pythnetwork/xc-admin-common": "workspace:*",
     "@solana/web3.js": "^1.73.0",
@@ -52,6 +52,7 @@
     "bs58": "^5.0.0",
     "extract-files": "^13.0.0",
     "fuels": "^0.94.0",
+    "near-api-js": "^3.0.2",
     "ramda": "^0.30.1",
     "starknet": "^6.9.0",
     "ts-node": "^10.9.1",

+ 87 - 0
contract_manager/src/chains.ts

@@ -36,6 +36,8 @@ import {
 } from "@ton/ton";
 import { keyPairFromSeed } from "@ton/crypto";
 import { PythContract } from "@pythnetwork/pyth-ton-js";
+import * as nearAPI from "near-api-js";
+import * as bs58 from "bs58";
 
 /**
  * Returns the chain rpc url with any environment variables replaced or throws an error if any are missing
@@ -858,3 +860,88 @@ export class TonChain extends Chain {
     return Number(balance) / 10 ** 9;
   }
 }
+
+export class NearChain extends Chain {
+  static type = "NearChain";
+
+  constructor(
+    id: string,
+    mainnet: boolean,
+    wormholeChainName: string,
+    nativeToken: TokenId | undefined,
+    private rpcUrl: string,
+    private networkId: string
+  ) {
+    super(id, mainnet, wormholeChainName, nativeToken);
+  }
+
+  static fromJson(parsed: ChainConfig): NearChain {
+    if (parsed.type !== NearChain.type) throw new Error("Invalid type");
+    return new NearChain(
+      parsed.id,
+      parsed.mainnet,
+      parsed.wormholeChainName,
+      parsed.nativeToken,
+      parsed.rpcUrl,
+      parsed.networkId
+    );
+  }
+
+  getType(): string {
+    return NearChain.type;
+  }
+
+  toJson(): KeyValueConfig {
+    return {
+      id: this.id,
+      wormholeChainName: this.wormholeChainName,
+      mainnet: this.mainnet,
+      type: NearChain.type,
+      rpcUrl: this.rpcUrl,
+      networkId: this.networkId,
+    };
+  }
+
+  /**
+   * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
+   * @param codeHash hex string of the 32 byte code hash for the new contract without the 0x prefix
+   */
+  generateGovernanceUpgradePayload(codeHash: string): Buffer {
+    return new UpgradeContract256Bit(this.wormholeChainName, codeHash).encode();
+  }
+
+  async getAccountAddress(privateKey: PrivateKey): Promise<string> {
+    return Buffer.from(
+      Ed25519Keypair.fromSecretKey(Buffer.from(privateKey, "hex"))
+        .getPublicKey()
+        .toRawBytes()
+    ).toString("hex");
+  }
+
+  async getAccountBalance(privateKey: PrivateKey): Promise<number> {
+    const accountId = await this.getAccountAddress(privateKey);
+    const account = await this.getNearAccount(accountId);
+    const balance = await account.getAccountBalance();
+    return Number(balance.available) / 1e24;
+  }
+
+  async getNearAccount(
+    accountId: string,
+    senderPrivateKey?: PrivateKey
+  ): Promise<nearAPI.Account> {
+    const keyStore = new nearAPI.keyStores.InMemoryKeyStore();
+    if (typeof senderPrivateKey !== "undefined") {
+      const key = bs58.encode(Buffer.from(senderPrivateKey, "hex"));
+      const keyPair = nearAPI.KeyPair.fromString(key);
+      const address = await this.getAccountAddress(senderPrivateKey);
+      await keyStore.setKey(this.networkId, address, keyPair);
+    }
+    const connectionConfig = {
+      networkId: this.networkId,
+      keyStore,
+      nodeUrl: this.rpcUrl,
+    };
+    const nearConnection = await nearAPI.connect(connectionConfig);
+    return await nearConnection.account(accountId);
+  }
+}

+ 264 - 0
contract_manager/src/contracts/near.ts

@@ -0,0 +1,264 @@
+import { DataSource } from "@pythnetwork/xc-admin-common";
+import {
+  KeyValueConfig,
+  PriceFeed,
+  PriceFeedContract,
+  PrivateKey,
+  TxResult,
+} from "../base";
+import { Chain, NearChain } from "../chains";
+import * as nearAPI from "near-api-js";
+import { BN } from "fuels";
+import { WormholeContract } from "./wormhole";
+
+export class NearWormholeContract extends WormholeContract {
+  static type = "NearWormholeContract";
+
+  constructor(public chain: NearChain, public address: string) {
+    super();
+  }
+
+  getId(): string {
+    return `${this.chain.getId()}__${this.address.replace(/-|\./g, "_")}`;
+  }
+
+  getChain(): NearChain {
+    return this.chain;
+  }
+
+  getType(): string {
+    return NearWormholeContract.type;
+  }
+
+  static fromJson(
+    chain: Chain,
+    parsed: { type: string; address: string }
+  ): NearWormholeContract {
+    if (parsed.type !== NearWormholeContract.type)
+      throw new Error("Invalid type");
+    if (!(chain instanceof NearChain))
+      throw new Error(`Wrong chain type ${chain}`);
+    return new NearWormholeContract(chain, parsed.address);
+  }
+
+  toJson(): KeyValueConfig {
+    return {
+      chain: this.chain.getId(),
+      address: this.address,
+      type: NearWormholeContract.type,
+    };
+  }
+
+  async upgradeGuardianSets(
+    senderPrivateKey: PrivateKey,
+    vaa: Buffer
+  ): Promise<TxResult> {
+    const senderAddress = await this.chain.getAccountAddress(senderPrivateKey);
+    const account = await this.chain.getNearAccount(
+      senderAddress,
+      senderPrivateKey
+    );
+    const outcome = await account.functionCall({
+      contractId: this.address,
+      methodName: "submit_vaa",
+      args: { vaa: vaa.toString("hex") },
+      gas: new BN(300e12),
+      attachedDeposit: new BN(1e12),
+    });
+    return { id: outcome.transaction.hash, info: outcome };
+  }
+
+  getCurrentGuardianSetIndex(): Promise<number> {
+    throw new Error(
+      "near wormhole contract doesn't implement getCurrentGuardianSetIndex method"
+    );
+  }
+  getChainId(): Promise<number> {
+    throw new Error(
+      "near wormhole contract doesn't implement getChainId method"
+    );
+  }
+  getGuardianSet(): Promise<string[]> {
+    throw new Error(
+      "near wormhole contract doesn't implement getGuardianSet method"
+    );
+  }
+}
+
+export class NearPriceFeedContract extends PriceFeedContract {
+  public static type = "NearPriceFeedContract";
+
+  constructor(public chain: NearChain, public address: string) {
+    super();
+  }
+
+  getId(): string {
+    return `${this.chain.getId()}__${this.address.replace(/-|\./g, "_")}`;
+  }
+
+  getType(): string {
+    return NearPriceFeedContract.type;
+  }
+
+  getChain(): NearChain {
+    return this.chain;
+  }
+
+  toJson(): KeyValueConfig {
+    return {
+      chain: this.chain.getId(),
+      address: this.address,
+      type: NearPriceFeedContract.type,
+    };
+  }
+
+  static fromJson(
+    chain: Chain,
+    parsed: { type: string; address: string }
+  ): NearPriceFeedContract {
+    if (parsed.type !== NearPriceFeedContract.type) {
+      throw new Error("Invalid type");
+    }
+    if (!(chain instanceof NearChain)) {
+      throw new Error(`Wrong chain type ${chain}`);
+    }
+    return new NearPriceFeedContract(chain, parsed.address);
+  }
+
+  async getContractNearAccount(
+    senderPrivateKey?: PrivateKey
+  ): Promise<nearAPI.Account> {
+    return await this.chain.getNearAccount(this.address, senderPrivateKey);
+  }
+
+  async getValidTimePeriod(): Promise<number> {
+    const account = await this.getContractNearAccount();
+    return account.viewFunction({
+      contractId: this.address,
+      methodName: "get_stale_threshold",
+    });
+  }
+
+  async getDataSources(): Promise<DataSource[]> {
+    const account = await this.getContractNearAccount();
+    const outcome: [{ emitter: number[]; chain: number }] =
+      await account.viewFunction({
+        contractId: this.address,
+        methodName: "get_sources",
+      });
+    return outcome.map((item) => {
+      return {
+        emitterChain: item.chain,
+        emitterAddress: Buffer.from(item.emitter).toString("hex"),
+      };
+    });
+  }
+
+  async getPriceFeed(feedId: string): Promise<PriceFeed | undefined> {
+    const account = await this.getContractNearAccount();
+    const price: {
+      price: string;
+      conf: string;
+      expo: number;
+      publish_time: number;
+    } | null = await account.viewFunction({
+      contractId: this.address,
+      methodName: "get_price_unsafe",
+      args: { price_identifier: feedId },
+    });
+    const emaPrice: {
+      price: string;
+      conf: string;
+      expo: number;
+      publish_time: number;
+    } | null = await account.viewFunction({
+      contractId: this.address,
+      methodName: "get_ema_price_unsafe",
+      args: { price_id: feedId },
+    });
+    if (price === null || emaPrice === null) {
+      return undefined;
+    } else {
+      return {
+        price: {
+          price: price.price,
+          conf: price.conf,
+          expo: price.expo.toString(),
+          publishTime: price.publish_time.toString(),
+        },
+        emaPrice: {
+          price: emaPrice.price,
+          conf: emaPrice.conf,
+          expo: emaPrice.expo.toString(),
+          publishTime: emaPrice.publish_time.toString(),
+        },
+      };
+    }
+  }
+
+  async executeUpdatePriceFeed(
+    senderPrivateKey: PrivateKey,
+    vaas: Buffer[]
+  ): Promise<TxResult> {
+    if (vaas.length === 0) {
+      throw new Error("no vaas specified");
+    }
+    const senderAddress = await this.chain.getAccountAddress(senderPrivateKey);
+    const account = await this.chain.getNearAccount(
+      senderAddress,
+      senderPrivateKey
+    );
+    const results = [];
+    for (const vaa of vaas) {
+      const outcome = await account.functionCall({
+        contractId: this.address,
+        methodName: "update_price_feeds",
+        args: { data: vaa.toString("hex") },
+        gas: new BN(300e12),
+        attachedDeposit: new BN(1e12),
+      });
+      results.push({ id: outcome.transaction.hash, info: outcome });
+    }
+    if (results.length === 1) {
+      return results[0];
+    } else {
+      return {
+        id: results.map((x) => x.id).join(","),
+        info: results.map((x) => x.info),
+      };
+    }
+  }
+
+  async executeGovernanceInstruction(
+    senderPrivateKey: PrivateKey,
+    vaa: Buffer
+  ): Promise<TxResult> {
+    const senderAddress = await this.chain.getAccountAddress(senderPrivateKey);
+    const account = await this.chain.getNearAccount(
+      senderAddress,
+      senderPrivateKey
+    );
+    const outcome = await account.functionCall({
+      contractId: this.address,
+      methodName: "execute_governance_instruction",
+      args: { vaa: vaa.toString("hex") },
+      gas: new BN(300e12),
+      attachedDeposit: new BN(1e12),
+    });
+    return { id: outcome.transaction.hash, info: outcome };
+  }
+
+  getBaseUpdateFee(): Promise<{ amount: string; denom?: string }> {
+    throw new Error("near contract doesn't implement getBaseUpdateFee method");
+  }
+  getLastExecutedGovernanceSequence(): Promise<number> {
+    throw new Error(
+      "near contract doesn't implement getLastExecutedGovernanceSequence method"
+    );
+  }
+  getGovernanceDataSource(): Promise<DataSource> {
+    throw new Error(
+      "near contract doesn't implement getGovernanceDataSource method"
+    );
+  }
+}

+ 5 - 0
contract_manager/src/store.ts

@@ -8,6 +8,7 @@ import {
   GlobalChain,
   SuiChain,
   TonChain,
+  NearChain,
 } from "./chains";
 import {
   AptosPriceFeedContract,
@@ -35,6 +36,7 @@ import {
   StarknetPriceFeedContract,
   StarknetWormholeContract,
 } from "./contracts/starknet";
+import { NearPriceFeedContract, NearWormholeContract } from "./contracts/near";
 
 export class Store {
   public chains: Record<string, Chain> = { global: new GlobalChain() };
@@ -85,6 +87,7 @@ export class Store {
       [FuelChain.type]: FuelChain,
       [StarknetChain.type]: StarknetChain,
       [TonChain.type]: TonChain,
+      [NearChain.type]: NearChain,
     };
 
     this.getYamlFiles(`${this.path}/chains/`).forEach((yamlFile) => {
@@ -156,6 +159,8 @@ export class Store {
       [StarknetWormholeContract.type]: StarknetWormholeContract,
       [TonPriceFeedContract.type]: TonPriceFeedContract,
       [TonWormholeContract.type]: TonWormholeContract,
+      [NearPriceFeedContract.type]: NearPriceFeedContract,
+      [NearWormholeContract.type]: NearWormholeContract,
     };
     this.getYamlFiles(`${this.path}/contracts/`).forEach((yamlFile) => {
       const parsedArray = parse(readFileSync(yamlFile, "utf-8"));

+ 12 - 0
contract_manager/store/chains/NearChains.yaml

@@ -0,0 +1,12 @@
+- id: near_testnet
+  wormholeChainName: near
+  mainnet: false
+  type: NearChain
+  rpcUrl: https://rpc.testnet.near.org
+  networkId: testnet
+- id: near
+  wormholeChainName: near
+  mainnet: true
+  type: NearChain
+  rpcUrl: https://rpc.mainnet.near.org
+  networkId: mainnet

+ 6 - 0
contract_manager/store/contracts/NearPriceFeedContracts.yaml

@@ -0,0 +1,6 @@
+- chain: near
+  address: pyth-oracle.near
+  type: NearPriceFeedContract
+- chain: near_testnet
+  address: pyth-oracle.testnet
+  type: NearPriceFeedContract

+ 6 - 0
contract_manager/store/contracts/NearWormholeContracts.yaml

@@ -0,0 +1,6 @@
+- chain: near
+  address: contract.wormhole_crypto.near
+  type: NearWormholeContract
+- chain: near_testnet
+  address: wormhole.wormhole.testnet
+  type: NearWormholeContract

+ 3 - 0
governance/xc_admin/packages/xc_admin_common/src/chains.ts

@@ -11,6 +11,9 @@ import { CHAINS as WORMHOLE_CHAINS } from "@certusone/wormhole-sdk";
 export const RECEIVER_CHAINS = {
   unset: 0, // The global chain id. For messages that are not chain specific.
 
+  // On the following networks we use Wormhole's contract
+  near: 15,
+
   // On the following networks we use our own version of Wormhole receiver contract
   ethereum: 2,
   bsc: 4,

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/governance_payload/UpgradeContract.ts

@@ -34,7 +34,7 @@ export class CosmosUpgradeContract extends PythGovernanceActionImpl {
   }
 }
 
-// Used by Aptos, Sui and Starknet
+// Used by Aptos, Sui, Near, and Starknet
 export class UpgradeContract256Bit extends PythGovernanceActionImpl {
   static layout: BufferLayout.Structure<Readonly<{ hash: string }>> =
     BufferLayout.struct([BufferLayoutExt.hexBytes(32, "hash")]);

+ 3 - 0
pnpm-lock.yaml

@@ -1015,6 +1015,9 @@ importers:
       fuels:
         specifier: ^0.94.0
         version: 0.94.5(encoding@0.1.13)
+      near-api-js:
+        specifier: ^3.0.2
+        version: 3.0.4(encoding@0.1.13)
       ramda:
         specifier: ^0.30.1
         version: 0.30.1