284 lines
8.1 KiB
TypeScript
284 lines
8.1 KiB
TypeScript
import { PriceFeedContract, PriceFeed, PrivateKey, TxResult } from "../base";
|
|
import { ApiError, BCS, CoinClient, TxnBuilderTypes } from "aptos";
|
|
import { AptosChain, Chain } from "../chains";
|
|
import { DataSource } from "xc_admin_common";
|
|
import { WormholeContract } from "./wormhole";
|
|
|
|
type WormholeState = {
|
|
chain_id: { number: string };
|
|
guardian_set_index: { number: string };
|
|
guardian_sets: { handle: string };
|
|
};
|
|
|
|
type GuardianSet = {
|
|
guardians: { address: { bytes: string } }[];
|
|
expiration_time: { number: string };
|
|
index: { number: string };
|
|
};
|
|
|
|
export class WormholeAptosContract extends WormholeContract {
|
|
constructor(public chain: AptosChain, public address: string) {
|
|
super();
|
|
}
|
|
|
|
async getState(): Promise<WormholeState> {
|
|
const client = this.chain.getClient();
|
|
const resources = await client.getAccountResources(this.address);
|
|
const type = "WormholeState";
|
|
for (const resource of resources) {
|
|
if (resource.type === `${this.address}::state::${type}`) {
|
|
return resource.data as WormholeState;
|
|
}
|
|
}
|
|
throw new Error(`${type} resource not found in account ${this.address}`);
|
|
}
|
|
|
|
async getCurrentGuardianSetIndex(): Promise<number> {
|
|
const data = await this.getState();
|
|
return Number(data.guardian_set_index.number);
|
|
}
|
|
|
|
async getChainId(): Promise<number> {
|
|
const data = await this.getState();
|
|
return Number(data.chain_id.number);
|
|
}
|
|
|
|
async getGuardianSet(): Promise<string[]> {
|
|
const data = await this.getState();
|
|
const client = this.chain.getClient();
|
|
const result = (await client.getTableItem(data.guardian_sets.handle, {
|
|
key_type: `u64`,
|
|
value_type: `${this.address}::structs::GuardianSet`,
|
|
key: data.guardian_set_index.number.toString(),
|
|
})) as GuardianSet;
|
|
return result.guardians.map((guardian) => guardian.address.bytes);
|
|
}
|
|
|
|
async upgradeGuardianSets(
|
|
senderPrivateKey: PrivateKey,
|
|
vaa: Buffer
|
|
): Promise<TxResult> {
|
|
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
|
|
TxnBuilderTypes.EntryFunction.natural(
|
|
`${this.address}::guardian_set_upgrade`,
|
|
"submit_vaa_entry",
|
|
[],
|
|
[BCS.bcsSerializeBytes(vaa)]
|
|
)
|
|
);
|
|
return this.chain.sendTransaction(senderPrivateKey, txPayload);
|
|
}
|
|
}
|
|
|
|
export class AptosPriceFeedContract extends PriceFeedContract {
|
|
static type = "AptosPriceFeedContract";
|
|
|
|
/**
|
|
* Given the ids of the pyth state and wormhole state, create a new AptosContract
|
|
* The package ids are derived based on the state ids
|
|
*
|
|
* @param chain the chain which this contract is deployed on
|
|
* @param stateId id of the pyth state for the deployed contract
|
|
* @param wormholeStateId id of the wormhole state for the wormhole contract that pyth binds to
|
|
*/
|
|
constructor(
|
|
public chain: AptosChain,
|
|
public stateId: string,
|
|
public wormholeStateId: string
|
|
) {
|
|
super();
|
|
}
|
|
|
|
static fromJson(
|
|
chain: Chain,
|
|
parsed: { type: string; stateId: string; wormholeStateId: string }
|
|
): AptosPriceFeedContract {
|
|
if (parsed.type !== AptosPriceFeedContract.type)
|
|
throw new Error("Invalid type");
|
|
if (!(chain instanceof AptosChain))
|
|
throw new Error(`Wrong chain type ${chain}`);
|
|
return new AptosPriceFeedContract(
|
|
chain,
|
|
parsed.stateId,
|
|
parsed.wormholeStateId
|
|
);
|
|
}
|
|
|
|
async executeGovernanceInstruction(
|
|
senderPrivateKey: PrivateKey,
|
|
vaa: Buffer
|
|
): Promise<TxResult> {
|
|
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
|
|
TxnBuilderTypes.EntryFunction.natural(
|
|
`${this.stateId}::governance`,
|
|
"execute_governance_instruction",
|
|
[],
|
|
[BCS.bcsSerializeBytes(vaa)]
|
|
)
|
|
);
|
|
return this.chain.sendTransaction(senderPrivateKey, txPayload);
|
|
}
|
|
|
|
public getWormholeContract(): WormholeAptosContract {
|
|
return new WormholeAptosContract(this.chain, this.wormholeStateId);
|
|
}
|
|
|
|
async executeUpdatePriceFeed(
|
|
senderPrivateKey: PrivateKey,
|
|
vaas: Buffer[]
|
|
): Promise<TxResult> {
|
|
const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
|
|
TxnBuilderTypes.EntryFunction.natural(
|
|
`${this.stateId}::pyth`,
|
|
"update_price_feeds_with_funder",
|
|
[],
|
|
[BCS.serializeVectorWithFunc(vaas, "serializeBytes")]
|
|
)
|
|
);
|
|
return this.chain.sendTransaction(senderPrivateKey, txPayload);
|
|
}
|
|
|
|
getStateResources() {
|
|
const client = this.chain.getClient();
|
|
return client.getAccountResources(this.stateId);
|
|
}
|
|
|
|
/**
|
|
* Returns the first occurrence of a resource with the given type in the pyth package state
|
|
* @param type
|
|
*/
|
|
async findResource(type: string) {
|
|
const resources = await this.getStateResources();
|
|
for (const resource of resources) {
|
|
if (resource.type === `${this.stateId}::state::${type}`) {
|
|
return resource.data;
|
|
}
|
|
}
|
|
throw new Error(`${type} resource not found in state ${this.stateId}`);
|
|
}
|
|
|
|
async getBaseUpdateFee() {
|
|
const data = (await this.findResource("BaseUpdateFee")) as { fee: string };
|
|
return { amount: data.fee };
|
|
}
|
|
|
|
getChain(): AptosChain {
|
|
return this.chain;
|
|
}
|
|
|
|
private parsePrice(priceInfo: {
|
|
expo: { magnitude: string; negative: boolean };
|
|
price: { magnitude: string; negative: boolean };
|
|
conf: string;
|
|
timestamp: string;
|
|
}) {
|
|
let expo = priceInfo.expo.magnitude;
|
|
if (priceInfo.expo.negative) expo = "-" + expo;
|
|
let price = priceInfo.price.magnitude;
|
|
if (priceInfo.price.negative) price = "-" + price;
|
|
return {
|
|
conf: priceInfo.conf,
|
|
publishTime: priceInfo.timestamp,
|
|
expo,
|
|
price,
|
|
};
|
|
}
|
|
|
|
async getPriceFeed(feedId: string): Promise<PriceFeed | undefined> {
|
|
const client = this.chain.getClient();
|
|
const res = (await this.findResource("LatestPriceInfo")) as {
|
|
info: { handle: string };
|
|
};
|
|
const handle = res.info.handle;
|
|
try {
|
|
const priceItemRes = await client.getTableItem(handle, {
|
|
key_type: `${this.stateId}::price_identifier::PriceIdentifier`,
|
|
value_type: `${this.stateId}::price_info::PriceInfo`,
|
|
key: {
|
|
bytes: feedId,
|
|
},
|
|
});
|
|
return {
|
|
price: this.parsePrice(priceItemRes.price_feed.price),
|
|
emaPrice: this.parsePrice(priceItemRes.price_feed.ema_price),
|
|
};
|
|
} catch (e) {
|
|
if (e instanceof ApiError && e.errorCode === "table_item_not_found")
|
|
return undefined;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async getDataSources(): Promise<DataSource[]> {
|
|
const data = (await this.findResource("DataSources")) as {
|
|
sources: {
|
|
keys: {
|
|
emitter_chain: string;
|
|
emitter_address: { external_address: string };
|
|
}[];
|
|
};
|
|
};
|
|
return data.sources.keys.map((source) => {
|
|
return {
|
|
emitterChain: Number(source.emitter_chain),
|
|
emitterAddress: source.emitter_address.external_address.replace(
|
|
"0x",
|
|
""
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
async getGovernanceDataSource(): Promise<DataSource> {
|
|
const data = (await this.findResource("GovernanceDataSource")) as {
|
|
source: {
|
|
emitter_chain: string;
|
|
emitter_address: { external_address: string };
|
|
};
|
|
};
|
|
return {
|
|
emitterChain: Number(data.source.emitter_chain),
|
|
emitterAddress: data.source.emitter_address.external_address.replace(
|
|
"0x",
|
|
""
|
|
),
|
|
};
|
|
}
|
|
|
|
async getLastExecutedGovernanceSequence() {
|
|
const data = (await this.findResource(
|
|
"LastExecutedGovernanceSequence"
|
|
)) as { sequence: string };
|
|
return Number(data.sequence);
|
|
}
|
|
|
|
getId(): string {
|
|
return `${this.chain.getId()}_${this.stateId}`;
|
|
}
|
|
|
|
getType(): string {
|
|
return AptosPriceFeedContract.type;
|
|
}
|
|
|
|
async getTotalFee(): Promise<bigint> {
|
|
const client = new CoinClient(this.chain.getClient());
|
|
return await client.checkBalance(this.stateId);
|
|
}
|
|
|
|
async getValidTimePeriod() {
|
|
const data = (await this.findResource("StalePriceThreshold")) as {
|
|
threshold_secs: string;
|
|
};
|
|
return Number(data.threshold_secs);
|
|
}
|
|
|
|
toJson() {
|
|
return {
|
|
chain: this.chain.getId(),
|
|
stateId: this.stateId,
|
|
wormholeStateId: this.wormholeStateId,
|
|
type: AptosPriceFeedContract.type,
|
|
};
|
|
}
|
|
}
|