diff --git a/contract_manager/package.json b/contract_manager/package.json index 4be1cd4f6d..676aed91bb 100644 --- a/contract_manager/package.json +++ b/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", diff --git a/contract_manager/src/chains.ts b/contract_manager/src/chains.ts index 3856fd4fb8..7dcef89d70 100644 --- a/contract_manager/src/chains.ts +++ b/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 { + return Buffer.from( + Ed25519Keypair.fromSecretKey(Buffer.from(privateKey, "hex")) + .getPublicKey() + .toRawBytes() + ).toString("hex"); + } + + async getAccountBalance(privateKey: PrivateKey): Promise { + 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 { + 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); + } +} diff --git a/contract_manager/src/contracts/near.ts b/contract_manager/src/contracts/near.ts new file mode 100644 index 0000000000..f5864074d6 --- /dev/null +++ b/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 { + 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 { + throw new Error( + "near wormhole contract doesn't implement getCurrentGuardianSetIndex method" + ); + } + getChainId(): Promise { + throw new Error( + "near wormhole contract doesn't implement getChainId method" + ); + } + getGuardianSet(): Promise { + 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 { + return await this.chain.getNearAccount(this.address, senderPrivateKey); + } + + async getValidTimePeriod(): Promise { + const account = await this.getContractNearAccount(); + return account.viewFunction({ + contractId: this.address, + methodName: "get_stale_threshold", + }); + } + + async getDataSources(): Promise { + 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 { + 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 { + 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 { + 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 { + throw new Error( + "near contract doesn't implement getLastExecutedGovernanceSequence method" + ); + } + getGovernanceDataSource(): Promise { + throw new Error( + "near contract doesn't implement getGovernanceDataSource method" + ); + } +} diff --git a/contract_manager/src/store.ts b/contract_manager/src/store.ts index fce34f386b..1c787e3a24 100644 --- a/contract_manager/src/store.ts +++ b/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 = { 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")); diff --git a/contract_manager/store/chains/NearChains.yaml b/contract_manager/store/chains/NearChains.yaml new file mode 100644 index 0000000000..84d0738e34 --- /dev/null +++ b/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 diff --git a/contract_manager/store/contracts/NearPriceFeedContracts.yaml b/contract_manager/store/contracts/NearPriceFeedContracts.yaml new file mode 100644 index 0000000000..f8a44b862a --- /dev/null +++ b/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 diff --git a/contract_manager/store/contracts/NearWormholeContracts.yaml b/contract_manager/store/contracts/NearWormholeContracts.yaml new file mode 100644 index 0000000000..8216fdc438 --- /dev/null +++ b/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 diff --git a/governance/xc_admin/packages/xc_admin_common/src/chains.ts b/governance/xc_admin/packages/xc_admin_common/src/chains.ts index 6bd31c44b2..715ca879de 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/chains.ts +++ b/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, diff --git a/governance/xc_admin/packages/xc_admin_common/src/governance_payload/UpgradeContract.ts b/governance/xc_admin/packages/xc_admin_common/src/governance_payload/UpgradeContract.ts index b5594d7503..2f42246034 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/governance_payload/UpgradeContract.ts +++ b/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> = BufferLayout.struct([BufferLayoutExt.hexBytes(32, "hash")]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 352beda5fd..c432f4d841 100644 --- a/pnpm-lock.yaml +++ b/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