Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion contract_manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
87 changes: 87 additions & 0 deletions contract_manager/src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 264 additions & 0 deletions contract_manager/src/contracts/near.ts
Original file line number Diff line number Diff line change
@@ -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> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so there is no way to check this out? strange.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is no method for this.

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"
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you know how the upgrade is going to work? we initially execute governance instruction, but what can we do after it :?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we'll need to call a permissionless method update_contract with the new wasm file as input.

}
Loading
Loading