diff --git a/contract_manager/package.json b/contract_manager/package.json index db435e86e0..502fbc8a9e 100644 --- a/contract_manager/package.json +++ b/contract_manager/package.json @@ -36,10 +36,13 @@ "@pythnetwork/pyth-sdk-solidity": "workspace:^", "@pythnetwork/pyth-starknet-js": "^0.2.1", "@pythnetwork/pyth-sui-js": "workspace:*", + "@pythnetwork/pyth-ton-js": "workspace:*", "@pythnetwork/solana-utils": "workspace:^", "@pythnetwork/xc-admin-common": "workspace:*", "@solana/web3.js": "^1.73.0", "@sqds/mesh": "^1.0.6", + "@ton/crypto": "^3.3.0", + "@ton/ton": "^15.1.0", "@types/yargs": "^17.0.32", "aptos": "^1.5.0", "axios": "^0.24.0", diff --git a/contract_manager/src/chains.ts b/contract_manager/src/chains.ts index caf28d1d24..189765eb81 100644 --- a/contract_manager/src/chains.ts +++ b/contract_manager/src/chains.ts @@ -26,6 +26,16 @@ import { TokenId } from "./token"; import { BN, Provider, Wallet, WalletUnlocked } from "fuels"; import { FUEL_ETH_ASSET_ID } from "@pythnetwork/pyth-fuel-js"; import { Contract, RpcProvider, Signer, ec, shortString } from "starknet"; +import { + TonClient, + WalletContractV4, + ContractProvider, + Address, + OpenedContract, + Sender, +} from "@ton/ton"; +import { keyPairFromSeed } from "@ton/crypto"; +import { PythContract } from "@pythnetwork/pyth-ton-js"; export type ChainConfig = Record & { mainnet: boolean; @@ -738,3 +748,103 @@ export class StarknetChain extends Chain { return new RpcProvider({ nodeUrl: this.rpcUrl }); } } + +export class TonChain extends Chain { + static type = "TonChain"; + + constructor( + id: string, + mainnet: boolean, + wormholeChainName: string, + nativeToken: TokenId | undefined, + public rpcUrl: string + ) { + super(id, mainnet, wormholeChainName, nativeToken); + } + + async getClient(): Promise { + // add apiKey if facing rate limit + const client = new TonClient({ + endpoint: this.rpcUrl, + }); + return client; + } + + async getContract(address: string): Promise> { + const client = await this.getClient(); + const contract = client.open( + PythContract.createFromAddress(Address.parse(address)) + ); + return contract; + } + + async getContractProvider(address: string): Promise { + const client = await this.getClient(); + return client.provider(Address.parse(address)); + } + + async getWallet(privateKey: PrivateKey): Promise { + const keyPair = keyPairFromSeed(Buffer.from(privateKey, "hex")); + return WalletContractV4.create({ + publicKey: keyPair.publicKey, + workchain: 0, + }); + } + + async getSender(privateKey: PrivateKey): Promise { + const client = await this.getClient(); + const keyPair = keyPairFromSeed(Buffer.from(privateKey, "hex")); + const wallet = WalletContractV4.create({ + publicKey: keyPair.publicKey, + workchain: 0, + }); + const provider = client.open(wallet); + return provider.sender(keyPair.secretKey); + } + + /** + * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain + * @param digest hex string of the 32 byte digest for the new package without the 0x prefix + */ + generateGovernanceUpgradePayload(digest: string): Buffer { + // This might throw an error because the Fuel contract doesn't support upgrades yet (blocked on Fuel releasing Upgradeability standard) + return new UpgradeContract256Bit(this.wormholeChainName, digest).encode(); + } + + getType(): string { + return TonChain.type; + } + + toJson(): KeyValueConfig { + return { + id: this.id, + wormholeChainName: this.wormholeChainName, + mainnet: this.mainnet, + rpcUrl: this.rpcUrl, + type: TonChain.type, + }; + } + + static fromJson(parsed: ChainConfig): TonChain { + if (parsed.type !== TonChain.type) throw new Error("Invalid type"); + return new TonChain( + parsed.id, + parsed.mainnet, + parsed.wormholeChainName, + parsed.nativeToken, + parsed.rpcUrl + ); + } + + async getAccountAddress(privateKey: PrivateKey): Promise { + const wallet = await this.getWallet(privateKey); + return wallet.address.toString(); + } + + async getAccountBalance(privateKey: PrivateKey): Promise { + const wallet = await this.getWallet(privateKey); + const provider = await this.getContractProvider(wallet.address.toString()); + const balance = await wallet.getBalance(provider); + return Number(balance) / 10 ** 9; + } +} diff --git a/contract_manager/src/contracts/index.ts b/contract_manager/src/contracts/index.ts index ea7145c336..7c19c8cd16 100644 --- a/contract_manager/src/contracts/index.ts +++ b/contract_manager/src/contracts/index.ts @@ -5,3 +5,4 @@ export * from "./fuel"; export * from "./sui"; export * from "./wormhole"; export * from "./evm_abis"; +export * from "./ton"; diff --git a/contract_manager/src/contracts/ton.ts b/contract_manager/src/contracts/ton.ts new file mode 100644 index 0000000000..94f1822d4e --- /dev/null +++ b/contract_manager/src/contracts/ton.ts @@ -0,0 +1,289 @@ +import { Chain, TonChain } from "../chains"; +import { WormholeContract } from "./wormhole"; +import { PriceFeed, PriceFeedContract, PrivateKey, TxResult } from "../base"; +import { TokenQty } from "../token"; +import { DataSource } from "@pythnetwork/xc-admin-common"; +import { Address, OpenedContract } from "@ton/ton"; +import { + BASE_UPDATE_PRICE_FEEDS_FEE, + PythContract, +} from "@pythnetwork/pyth-ton-js"; + +export class TonWormholeContract extends WormholeContract { + static type = "TonWormholeContract"; + + getId(): string { + return `${this.chain.getId()}_${this.address}_${TonWormholeContract.type}`; + } + + getChain(): TonChain { + return this.chain; + } + + getType(): string { + return TonWormholeContract.type; + } + + toJson() { + return { + chain: this.chain.getId(), + address: this.address, + type: TonWormholeContract.type, + }; + } + + static fromJson( + chain: Chain, + parsed: { + type: string; + address: string; + } + ): TonWormholeContract { + if (parsed.type !== TonWormholeContract.type) + throw new Error("Invalid type"); + if (!(chain instanceof TonChain)) + throw new Error(`Wrong chain type ${chain}`); + return new TonWormholeContract(chain, parsed.address); + } + + constructor(public chain: TonChain, public address: string) { + super(); + } + + async getContract(): Promise> { + const provider = await this.chain.getContractProvider(this.address); + const contract = provider.open( + PythContract.createFromAddress(Address.parse(this.address)) + ); + + return contract; + } + + async getCurrentGuardianSetIndex(): Promise { + const contract = await this.getContract(); + const result = await contract.getCurrentGuardianSetIndex(); + return result; + } + + async getChainId(): Promise { + const contract = await this.getContract(); + const result = await contract.getChainId(); + return Number(result); + } + + async getGuardianSet(): Promise { + const contract = await this.getContract(); + const guardianSetIndex = await this.getCurrentGuardianSetIndex(); + const result = await contract.getGuardianSet(guardianSetIndex); + return result.keys; + } + + async upgradeGuardianSets( + senderPrivateKey: PrivateKey, + vaa: Buffer + ): Promise { + const contract = await this.getContract(); + const provider = await this.chain.getContractProvider(this.address); + const sender = await this.chain.getSender(senderPrivateKey); + const wallet = await this.chain.getWallet(senderPrivateKey); + await contract.sendUpdateGuardianSet(sender, vaa); + + // Get recent transactions for this address + const transactions = await provider.getTransactions( + wallet.address, + BigInt(0), + Buffer.alloc(0), + 1 + ); + + return { + id: transactions[0].hash.toString(), + info: JSON.stringify("0x1"), + }; + } +} + +export class TonPriceFeedContract extends PriceFeedContract { + static type = "TonPriceFeedContract"; + + constructor(public chain: TonChain, public address: string) { + super(); + } + + static fromJson( + chain: Chain, + parsed: { type: string; address: string } + ): TonPriceFeedContract { + if (parsed.type !== TonPriceFeedContract.type) + throw new Error("Invalid type"); + if (!(chain instanceof TonChain)) + throw new Error(`Wrong chain type ${chain}`); + return new TonPriceFeedContract(chain, parsed.address); + } + + getId(): string { + return `${this.chain.getId()}_${this.address}_${TonPriceFeedContract.type}`; + } + + getChain(): TonChain { + return this.chain; + } + + getType(): string { + return TonPriceFeedContract.type; + } + + async getContract(): Promise> { + const provider = await this.chain.getContractProvider(this.address); + const contract = provider.open( + PythContract.createFromAddress(Address.parse(this.address)) + ); + + return contract; + } + + async getTotalFee(): Promise { + const client = await this.chain.getClient(); + const balance = await client.getBalance(Address.parse(this.address)); + return { + amount: balance, + denom: this.chain.getNativeToken(), + }; + } + + async getLastExecutedGovernanceSequence(): Promise { + const contract = await this.getContract(); + const result = await contract.getLastExecutedGovernanceSequence(); + return Number(result); + } + + async getPriceFeed(feedId: string): Promise { + const contract = await this.getContract(); + const feedIdWithPrefix = `0x${feedId}`; + try { + const price = await contract.getPriceUnsafe(feedIdWithPrefix); + const emaPrice = await contract.getEmaPriceUnsafe(feedIdWithPrefix); + return { + price: { + price: price.price.toString(), + conf: price.conf.toString(), + expo: price.expo.toString(), + publishTime: price.publishTime.toString(), + }, + emaPrice: { + price: emaPrice.price.toString(), + conf: emaPrice.conf.toString(), + expo: emaPrice.expo.toString(), + publishTime: emaPrice.publishTime.toString(), + }, + }; + } catch (e) { + console.error(e); + return undefined; + } + } + + async getValidTimePeriod(): Promise { + // Not supported but return 1 because it's required by the abstract class + return 1; + } + + async getWormholeContract(): Promise { + // Price feed contract and wormhole contract live at same address in TON + return new TonWormholeContract(this.chain, this.address); + } + + async getBaseUpdateFee() { + const contract = await this.getContract(); + const amount = await contract.getSingleUpdateFee(); + return { + amount: amount.toString(), + denom: this.chain.getNativeToken(), + }; + } + + async getDataSources(): Promise { + const contract = await this.getContract(); + const dataSources = await contract.getDataSources(); + return dataSources.map((ds: DataSource) => ({ + emitterChain: ds.emitterChain, + emitterAddress: ds.emitterAddress.replace("0x", ""), + })); + } + + async getGovernanceDataSource(): Promise { + const contract = await this.getContract(); + const result = await contract.getGovernanceDataSource(); + if (result === null) { + throw new Error("Governance data source not found"); + } + return { + emitterChain: result.emitterChain, + emitterAddress: result.emitterAddress, + }; + } + + async executeUpdatePriceFeed( + senderPrivateKey: PrivateKey, + vaas: Buffer[] + ): Promise { + const client = await this.chain.getClient(); + const contract = await this.getContract(); + const wallet = await this.chain.getWallet(senderPrivateKey); + const sender = await this.chain.getSender(senderPrivateKey); + for (const vaa of vaas) { + const fee = await contract.getUpdateFee(vaa); + console.log(fee); + await contract.sendUpdatePriceFeeds( + sender, + vaa, + BASE_UPDATE_PRICE_FEEDS_FEE + BigInt(fee) + ); + } + + const txDetails = await client.getTransactions(wallet.address, { + limit: 1, + }); + const txHash = Buffer.from(txDetails[0].hash()).toString("hex"); + const txInfo = JSON.stringify(txDetails[0].description, (_, value) => + typeof value === "bigint" ? value.toString() : value + ); + + return { + id: txHash, + info: txInfo, + }; + } + + async executeGovernanceInstruction( + senderPrivateKey: PrivateKey, + vaa: Buffer + ): Promise { + const client = await this.chain.getClient(); + const contract = await this.getContract(); + const wallet = await this.chain.getWallet(senderPrivateKey); + const sender = await this.chain.getSender(senderPrivateKey); + await contract.sendExecuteGovernanceAction(sender, vaa); + + const txDetails = await client.getTransactions(wallet.address, { + limit: 1, + }); + const txHash = Buffer.from(txDetails[0].hash()).toString("hex"); + const txInfo = JSON.stringify(txDetails[0].description, (_, value) => + typeof value === "bigint" ? value.toString() : value + ); + + return { + id: txHash, + info: txInfo, + }; + } + + toJson() { + return { + chain: this.chain.getId(), + address: this.address, + type: TonPriceFeedContract.type, + }; + } +} diff --git a/contract_manager/src/store.ts b/contract_manager/src/store.ts index f28a101874..fce34f386b 100644 --- a/contract_manager/src/store.ts +++ b/contract_manager/src/store.ts @@ -7,6 +7,7 @@ import { FuelChain, GlobalChain, SuiChain, + TonChain, } from "./chains"; import { AptosPriceFeedContract, @@ -22,6 +23,8 @@ import { WormholeContract, FuelPriceFeedContract, EvmExpressRelayContract, + TonPriceFeedContract, + TonWormholeContract, } from "./contracts"; import { Token } from "./token"; import { PriceFeedContract, Storable } from "./base"; @@ -81,6 +84,7 @@ export class Store { [AptosChain.type]: AptosChain, [FuelChain.type]: FuelChain, [StarknetChain.type]: StarknetChain, + [TonChain.type]: TonChain, }; this.getYamlFiles(`${this.path}/chains/`).forEach((yamlFile) => { @@ -150,6 +154,8 @@ export class Store { [FuelWormholeContract.type]: FuelWormholeContract, [StarknetPriceFeedContract.type]: StarknetPriceFeedContract, [StarknetWormholeContract.type]: StarknetWormholeContract, + [TonPriceFeedContract.type]: TonPriceFeedContract, + [TonWormholeContract.type]: TonWormholeContract, }; this.getYamlFiles(`${this.path}/contracts/`).forEach((yamlFile) => { const parsedArray = parse(readFileSync(yamlFile, "utf-8")); diff --git a/contract_manager/store/chains/TonChains.yaml b/contract_manager/store/chains/TonChains.yaml new file mode 100644 index 0000000000..40b4e769c6 --- /dev/null +++ b/contract_manager/store/chains/TonChains.yaml @@ -0,0 +1,5 @@ +- id: ton_testnet + wormholeChainName: ton_testnet + mainnet: false + rpcUrl: https://testnet.toncenter.com/api/v2/jsonRPC + type: TonChain diff --git a/contract_manager/store/contracts/TonPriceFeedContracts.yaml b/contract_manager/store/contracts/TonPriceFeedContracts.yaml new file mode 100644 index 0000000000..e1ec02fd9d --- /dev/null +++ b/contract_manager/store/contracts/TonPriceFeedContracts.yaml @@ -0,0 +1,3 @@ +- chain: ton_testnet + address: "EQDwGkJmcj7MMmWAHmhldnY-lAKI6hcTQ2tAEcapmwCnztQU" + type: TonPriceFeedContract diff --git a/contract_manager/store/contracts/TonWormholeContracts.yaml b/contract_manager/store/contracts/TonWormholeContracts.yaml new file mode 100644 index 0000000000..b51348614b --- /dev/null +++ b/contract_manager/store/contracts/TonWormholeContracts.yaml @@ -0,0 +1,3 @@ +- chain: ton_testnet + address: "EQDwGkJmcj7MMmWAHmhldnY-lAKI6hcTQ2tAEcapmwCnztQU" + type: TonWormholeContract 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 41e288b4a9..8fe825e111 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/chains.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/chains.ts @@ -199,6 +199,7 @@ export const RECEIVER_CHAINS = { sanko_testnet: 50101, skate_testnet: 50102, movement_porto_testnet: 50103, + ton_testnet: 50104, }; // If there is any overlapping value the receiver chain will replace the wormhole diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5698fafe8f..85e440e22f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -530,6 +530,9 @@ importers: '@pythnetwork/pyth-sui-js': specifier: workspace:* version: link:../target_chains/sui/sdk/js + '@pythnetwork/pyth-ton-js': + specifier: workspace:* + version: link:../target_chains/ton/sdk/js '@pythnetwork/solana-utils': specifier: workspace:^ version: link:../target_chains/solana/sdk/js/solana_utils @@ -542,6 +545,12 @@ importers: '@sqds/mesh': specifier: ^1.0.6 version: 1.0.6(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@ton/crypto': + specifier: ^3.3.0 + version: 3.3.0 + '@ton/ton': + specifier: ^15.1.0 + version: 15.1.0(@ton/core@0.59.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) '@types/yargs': specifier: ^17.0.32 version: 17.0.32 @@ -32353,7 +32362,7 @@ snapshots: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.0.0 '@noble/hashes': 1.5.0 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 '@types/debug': 4.1.12 debug: 4.3.7(supports-color@8.1.1) pony-cause: 2.1.11 diff --git a/target_chains/ton/contracts/contracts/Pyth.fc b/target_chains/ton/contracts/contracts/Pyth.fc index 69d9f39d67..9addd0cd90 100644 --- a/target_chains/ton/contracts/contracts/Pyth.fc +++ b/target_chains/ton/contracts/contracts/Pyth.fc @@ -80,6 +80,11 @@ int get_governance_data_source_index() method_id { return governance_data_source_index; } +cell get_data_sources() method_id { + load_data(); + return data_sources; +} + cell get_governance_data_source() method_id { load_data(); return governance_data_source; diff --git a/target_chains/ton/contracts/contracts/tests/PythTest.fc b/target_chains/ton/contracts/contracts/tests/PythTest.fc index d65bab5c40..008f2cc535 100644 --- a/target_chains/ton/contracts/contracts/tests/PythTest.fc +++ b/target_chains/ton/contracts/contracts/tests/PythTest.fc @@ -77,3 +77,7 @@ (int) test_get_is_valid_data_source(cell data_source) method_id { return get_is_valid_data_source(data_source); } + +(cell) test_get_data_sources() method_id { + return get_data_sources(); +} diff --git a/target_chains/ton/contracts/tests/PythTest.spec.ts b/target_chains/ton/contracts/tests/PythTest.spec.ts index 00cf001389..adeb19673e 100644 --- a/target_chains/ton/contracts/tests/PythTest.spec.ts +++ b/target_chains/ton/contracts/tests/PythTest.spec.ts @@ -21,13 +21,14 @@ import { } from "./utils/pyth"; import { GUARDIAN_SET_0, MAINNET_UPGRADE_VAAS } from "./utils/wormhole"; import { DataSource } from "@pythnetwork/xc-admin-common"; -import { parseDataSource, createAuthorizeUpgradePayload } from "./utils"; +import { createAuthorizeUpgradePayload } from "./utils"; import { UniversalAddress, createVAA, serialize, } from "@wormhole-foundation/sdk-definitions"; import { mocks } from "@wormhole-foundation/sdk-definitions/testing"; +import { BASE_UPDATE_PRICE_FEEDS_FEE } from "@pythnetwork/pyth-ton-js"; const TIME_PERIOD = 60; const PRICE = new Price({ @@ -86,7 +87,6 @@ describe("PythTest", () => { async function deployContract( priceFeedId: HexString = BTC_PRICE_FEED_ID, - timePeriod: number = TIME_PERIOD, price: Price = PRICE, emaPrice: Price = EMA_PRICE, singleUpdateFee: number = SINGLE_UPDATE_FEE, @@ -100,7 +100,6 @@ describe("PythTest", () => { ) { const config: PythTestConfig = { priceFeedId, - timePeriod, price, emaPrice, singleUpdateFee, @@ -163,7 +162,7 @@ describe("PythTest", () => { expo: 3, publishTime: timeNow, }); - await deployContract(BTC_PRICE_FEED_ID, TIME_PERIOD, price, EMA_PRICE); + await deployContract(BTC_PRICE_FEED_ID, price, EMA_PRICE); const result = await pythTest.getPriceNoOlderThan( TIME_PERIOD, @@ -192,7 +191,7 @@ describe("PythTest", () => { expo: 7, publishTime: timeNow, }); - await deployContract(BTC_PRICE_FEED_ID, TIME_PERIOD, PRICE, emaPrice); + await deployContract(BTC_PRICE_FEED_ID, PRICE, emaPrice); const result = await pythTest.getEmaPriceNoOlderThan( TIME_PERIOD, @@ -331,7 +330,6 @@ describe("PythTest", () => { it("should fail to update price feeds with invalid data source", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -364,7 +362,7 @@ describe("PythTest", () => { expo: 3, publishTime: staleTime, }); - await deployContract(BTC_PRICE_FEED_ID, TIME_PERIOD, stalePrice, EMA_PRICE); + await deployContract(BTC_PRICE_FEED_ID, stalePrice, EMA_PRICE); await expect( pythTest.getPriceNoOlderThan(TIME_PERIOD, BTC_PRICE_FEED_ID) @@ -405,7 +403,7 @@ describe("PythTest", () => { const result = await pythTest.sendUpdatePriceFeeds( deployer.getSender(), updateData, - 156000000n + BigInt(insufficientFee) // 156000000 = 390000 (estimated gas used for the transaction, this is defined in contracts/common/gas.fc as UPDATE_PRICE_FEEDS_GAS) * 400 (current settings in basechain are as follows: 1 unit of gas costs 400 nanotons) + BASE_UPDATE_PRICE_FEEDS_FEE + BigInt(insufficientFee) ); // Check that the transaction did not succeed @@ -446,7 +444,6 @@ describe("PythTest", () => { it("should correctly get last executed governance sequence", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -478,7 +475,6 @@ describe("PythTest", () => { // Deploy contract with initial governance data source await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -512,14 +508,11 @@ describe("PythTest", () => { // Check initial value (should be empty) let result = await pythTest.getGovernanceDataSource(); - expect(result).toBeDefined(); - expect(result.bits.length).toBe(0); - expect(result.refs.length).toBe(0); + expect(result).toEqual(null); // Deploy contract with initial governance data source await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -534,8 +527,7 @@ describe("PythTest", () => { // Check that the governance data source is set result = await pythTest.getGovernanceDataSource(); - let dataSource = parseDataSource(result); - expect(dataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[0]); + expect(result).toEqual(TEST_GOVERNANCE_DATA_SOURCES[0]); // Execute governance action to change data source await pythTest.sendExecuteGovernanceAction( @@ -545,8 +537,7 @@ describe("PythTest", () => { // Check that the data source has changed result = await pythTest.getGovernanceDataSource(); - dataSource = parseDataSource(result); - expect(dataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[1]); + expect(result).toEqual(TEST_GOVERNANCE_DATA_SOURCES[1]); }); it("should correctly get single update fee", async () => { @@ -561,7 +552,6 @@ describe("PythTest", () => { it("should execute set data sources governance instruction", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -615,7 +605,6 @@ describe("PythTest", () => { it("should execute set fee governance instruction", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -656,7 +645,6 @@ describe("PythTest", () => { it("should execute authorize governance data source transfer", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -674,8 +662,7 @@ describe("PythTest", () => { expect(initialIndex).toEqual(0); // Initial value should be 0 // Get the initial governance data source - const initialDataSourceCell = await pythTest.getGovernanceDataSource(); - const initialDataSource = parseDataSource(initialDataSourceCell); + const initialDataSource = await pythTest.getGovernanceDataSource(); expect(initialDataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[0]); // Get the initial last executed governance sequence @@ -698,8 +685,7 @@ describe("PythTest", () => { expect(newIndex).toEqual(1); // The new index value should match the one in the test payload // Get the new governance data source - const newDataSourceCell = await pythTest.getGovernanceDataSource(); - const newDataSource = parseDataSource(newDataSourceCell); + const newDataSource = await pythTest.getGovernanceDataSource(); expect(newDataSource).not.toEqual(initialDataSource); // The data source should have changed expect(newDataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[1]); // The data source should have changed @@ -712,7 +698,6 @@ describe("PythTest", () => { it("should fail when executing request governance data source transfer directly", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -743,15 +728,13 @@ describe("PythTest", () => { expect(index).toEqual(0); // Should still be the initial value // Verify that the governance data source hasn't changed - const dataSourceCell = await pythTest.getGovernanceDataSource(); - const dataSource = parseDataSource(dataSourceCell); + const dataSource = await pythTest.getGovernanceDataSource(); expect(dataSource).toEqual(TEST_GOVERNANCE_DATA_SOURCES[1]); // Should still be the initial value }); it("should fail to execute governance action with invalid governance data source", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -780,7 +763,6 @@ describe("PythTest", () => { it("should fail to execute governance action with old sequence number", async () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -817,7 +799,6 @@ describe("PythTest", () => { const invalidChainId = 999; await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -869,7 +850,6 @@ describe("PythTest", () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -938,7 +918,6 @@ describe("PythTest", () => { await deployContract( BTC_PRICE_FEED_ID, - TIME_PERIOD, PRICE, EMA_PRICE, SINGLE_UPDATE_FEE, @@ -982,4 +961,11 @@ describe("PythTest", () => { // Verify that the contract has not been upgraded by attempting to call the new method await expect(pythTest.getNewFunction()).rejects.toThrow(); }); + + it("should correctly get data sources", async () => { + await deployContract(); + + const dataSources = await pythTest.getDataSources(); + expect(dataSources).toEqual(DATA_SOURCES); + }); }); diff --git a/target_chains/ton/contracts/tests/utils.ts b/target_chains/ton/contracts/tests/utils.ts index cae3fa6cba..e7b5221ed8 100644 --- a/target_chains/ton/contracts/tests/utils.ts +++ b/target_chains/ton/contracts/tests/utils.ts @@ -1,5 +1,4 @@ -import { DataSource } from "@pythnetwork/xc-admin-common"; -import { Cell, Transaction } from "@ton/core"; +import { Transaction } from "@ton/core"; import { Buffer } from "buffer"; const GOVERNANCE_MAGIC = 0x5054474d; @@ -7,14 +6,6 @@ const GOVERNANCE_MODULE = 1; const AUTHORIZE_UPGRADE_CONTRACT_ACTION = 0; const TARGET_CHAIN_ID = 1; -// Helper function to parse DataSource from a Cell -export function parseDataSource(cell: Cell): DataSource { - const slice = cell.beginParse(); - const emitterChain = slice.loadUint(16); - const emitterAddress = slice.loadUint(256).toString(16).padStart(64, "0"); - return { emitterChain, emitterAddress }; -} - function computedGeneric(transaction: Transaction) { if (transaction.description.type !== "generic") throw "Expected generic transactionaction"; diff --git a/target_chains/ton/contracts/tests/utils/wormhole.ts b/target_chains/ton/contracts/tests/utils/wormhole.ts index 097493a691..c81e6c0c9a 100644 --- a/target_chains/ton/contracts/tests/utils/wormhole.ts +++ b/target_chains/ton/contracts/tests/utils/wormhole.ts @@ -163,28 +163,6 @@ export function createGuardianSetsDict( return guardianSets; } -export function parseGuardianSetKeys(cell: Cell): string[] { - const keys: string[] = []; - - function parseCell(c: Cell) { - let slice = c.beginParse(); - while (slice.remainingRefs > 0 || slice.remainingBits >= 160) { - if (slice.remainingBits >= 160) { - const bitsToSkip = slice.remainingBits - 160; - slice = slice.skip(bitsToSkip); - const key = slice.loadBits(160); - keys.push("0x" + key.toString()); - } - if (slice.remainingRefs > 0) { - parseCell(slice.loadRef()); - } - } - } - - parseCell(cell); - return keys; -} - // Taken from https://github.com/pyth-network/pyth-crosschain/blob/main/contract_manager/src/contracts/wormhole.ts#L32-L37 export const MAINNET_UPGRADE_VAAS = [ "010000000001007ac31b282c2aeeeb37f3385ee0de5f8e421d30b9e5ae8ba3d4375c1c77a86e77159bb697d9c456d6f8c02d22a94b1279b65b0d6a9957e7d3857423845ac758e300610ac1d2000000030001000000000000000000000000000000000000000000000000000000000000000400000000000005390000000000000000000000000000000000000000000000000000000000436f7265020000000000011358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cdeb5f7389fa26941519f0863349c223b73a6ddee774a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d", diff --git a/target_chains/ton/contracts/wrappers/BaseWrapper.ts b/target_chains/ton/contracts/wrappers/BaseWrapper.ts index 548534858f..65008df410 100644 --- a/target_chains/ton/contracts/wrappers/BaseWrapper.ts +++ b/target_chains/ton/contracts/wrappers/BaseWrapper.ts @@ -26,7 +26,6 @@ export class BaseWrapper implements Contract { static createInitData(config: { priceFeedId?: HexString; - timePeriod?: number; price?: Price; emaPrice?: Price; singleUpdateFee?: number; @@ -43,12 +42,7 @@ export class BaseWrapper implements Contract { Dictionary.Values.Cell() ); - if ( - config.priceFeedId && - config.price && - config.emaPrice && - config.timePeriod - ) { + if (config.priceFeedId && config.price && config.emaPrice) { const priceCell = beginCell() .storeInt( config.price.getPriceAsNumberUnchecked() * 10 ** -config.price.expo, @@ -80,7 +74,6 @@ export class BaseWrapper implements Contract { const priceFeedCell = beginCell() .storeRef(priceCell) .storeRef(emaPriceCell) - .storeUint(config.timePeriod, 32) .endCell(); priceDict.set(BigInt(config.priceFeedId), priceFeedCell); @@ -88,7 +81,7 @@ export class BaseWrapper implements Contract { // Create a dictionary for data sources const dataSourcesDict = Dictionary.empty( - Dictionary.Keys.Uint(32), + Dictionary.Keys.Uint(8), Dictionary.Values.Cell() ); // Create a dictionary for valid data sources diff --git a/target_chains/ton/contracts/wrappers/PythTest.ts b/target_chains/ton/contracts/wrappers/PythTest.ts index 809219dcef..cf4133ae19 100644 --- a/target_chains/ton/contracts/wrappers/PythTest.ts +++ b/target_chains/ton/contracts/wrappers/PythTest.ts @@ -9,12 +9,15 @@ import { } from "@ton/core"; import { BaseWrapper } from "./BaseWrapper"; import { HexString, Price } from "@pythnetwork/price-service-sdk"; -import { createCellChain } from "@pythnetwork/pyth-ton-js"; +import { + createCellChain, + parseDataSource, + parseDataSources, +} from "@pythnetwork/pyth-ton-js"; import { DataSource } from "@pythnetwork/xc-admin-common"; export type PythTestConfig = { priceFeedId: HexString; - timePeriod: number; price: Price; emaPrice: Price; singleUpdateFee: number; @@ -135,7 +138,7 @@ export class PythTest extends BaseWrapper { async getGovernanceDataSource(provider: ContractProvider) { const result = await provider.get("test_get_governance_data_source", []); - return result.stack.readCell(); + return parseDataSource(result.stack.readCell()); } async sendExecuteGovernanceAction( @@ -188,6 +191,11 @@ export class PythTest extends BaseWrapper { return result.stack.readBoolean(); } + async getDataSources(provider: ContractProvider) { + const result = await provider.get("test_get_data_sources", []); + return parseDataSources(result.stack.readCell()); + } + async getNewFunction(provider: ContractProvider) { const result = await provider.get("test_new_function", []); return result.stack.readNumber(); diff --git a/target_chains/ton/contracts/wrappers/WormholeTest.ts b/target_chains/ton/contracts/wrappers/WormholeTest.ts index 946e5f00b6..d7bd522897 100644 --- a/target_chains/ton/contracts/wrappers/WormholeTest.ts +++ b/target_chains/ton/contracts/wrappers/WormholeTest.ts @@ -1,7 +1,9 @@ import { Cell, contractAddress, ContractProvider, Sender } from "@ton/core"; import { BaseWrapper } from "./BaseWrapper"; -import { createCellChain } from "@pythnetwork/pyth-ton-js"; -import { parseGuardianSetKeys } from "../tests/utils/wormhole"; +import { + createCellChain, + parseGuardianSetKeys, +} from "@pythnetwork/pyth-ton-js"; export type WormholeTestConfig = { guardianSetIndex: number; diff --git a/target_chains/ton/sdk/js/README.md b/target_chains/ton/sdk/js/README.md index da66eedcd7..696d1739ca 100644 --- a/target_chains/ton/sdk/js/README.md +++ b/target_chains/ton/sdk/js/README.md @@ -73,7 +73,7 @@ async function main() { await contract.sendUpdatePriceFeeds( provider.sender(key.secretKey), updateData, - 156000000n + BigInt(updateFee) // 156000000 = 390000 (estimated gas used for the transaction, this is defined in contracts/common/gas.fc as UPDATE_PRICE_FEEDS_GAS) * 400 (current settings in basechain are as follows: 1 unit of gas costs 400 nanotons) + BASE_UPDATE_PRICE_FEEDS_FEE + BigInt(updateFee) ); console.log("Price feeds updated successfully."); diff --git a/target_chains/ton/sdk/js/src/index.ts b/target_chains/ton/sdk/js/src/index.ts index 7d6fdfda5e..4ab7b021a3 100644 --- a/target_chains/ton/sdk/js/src/index.ts +++ b/target_chains/ton/sdk/js/src/index.ts @@ -3,13 +3,26 @@ import { beginCell, Cell, Contract, + Dictionary, Sender, SendMode, + toNano, } from "@ton/core"; import { ContractProvider } from "@ton/ton"; export const PYTH_CONTRACT_ADDRESS_TESTNET = "EQDwGkJmcj7MMmWAHmhldnY-lAKI6hcTQ2tAEcapmwCnztQU"; +// This is defined in target_chains/ton/contracts/common/gas.fc +export const UPDATE_PRICE_FEEDS_GAS = 390000n; +// Current settings in basechain are as follows: 1 unit of gas costs 400 nanotons +export const GAS_PRICE_FACTOR = 400n; +export const BASE_UPDATE_PRICE_FEEDS_FEE = + UPDATE_PRICE_FEEDS_GAS * GAS_PRICE_FACTOR; + +export interface DataSource { + emitterChain: number; + emitterAddress: string; +} export class PythContract implements Contract { constructor( @@ -27,6 +40,23 @@ export class PythContract implements Contract { return result.stack.readNumber(); } + async sendUpdateGuardianSet( + provider: ContractProvider, + via: Sender, + vm: Buffer + ) { + const messageBody = beginCell() + .storeUint(1, 32) // OP_UPDATE_GUARDIAN_SET + .storeRef(createCellChain(vm)) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + async sendUpdatePriceFeeds( provider: ContractProvider, via: Sender, @@ -45,6 +75,23 @@ export class PythContract implements Contract { }); } + async sendExecuteGovernanceAction( + provider: ContractProvider, + via: Sender, + governanceAction: Buffer + ) { + const messageBody = beginCell() + .storeUint(3, 32) // OP_EXECUTE_GOVERNANCE_ACTION + .storeRef(createCellChain(governanceAction)) + .endCell(); + + await provider.internal(via, { + value: toNano("0.1"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: messageBody, + }); + } + async getPriceUnsafe(provider: ContractProvider, priceFeedId: string) { const result = await provider.get("get_price_unsafe", [ { type: "int", value: BigInt(priceFeedId) }, @@ -140,6 +187,47 @@ export class PythContract implements Contract { return result.stack.readNumber(); } + + async getLastExecutedGovernanceSequence(provider: ContractProvider) { + const result = await provider.get( + "get_last_executed_governance_sequence", + [] + ); + + return result.stack.readNumber(); + } + + async getChainId(provider: ContractProvider) { + const result = await provider.get("get_chain_id", []); + + return result.stack.readNumber(); + } + + async getDataSources(provider: ContractProvider) { + const result = await provider.get("get_data_sources", []); + return parseDataSources(result.stack.readCell()); + } + + async getGovernanceDataSource(provider: ContractProvider) { + const result = await provider.get("get_governance_data_source", []); + return parseDataSource(result.stack.readCell()); + } + + async getGuardianSet(provider: ContractProvider, index: number) { + const result = await provider.get("get_guardian_set", [ + { type: "int", value: BigInt(index) }, + ]); + + const expirationTime = result.stack.readNumber(); + const keys = parseGuardianSetKeys(result.stack.readCell()); + const keyCount = result.stack.readNumber(); + + return { + expirationTime, + keys, + keyCount, + }; + } } export function createCellChain(buffer: Buffer): Cell { @@ -182,3 +270,51 @@ function bufferToChunks(buff: Buffer, chunkSizeBytes = 127): Uint8Array[] { return chunks; } + +export function parseDataSources(cell: Cell): DataSource[] { + const dataSources: DataSource[] = []; + const slice = cell.beginParse(); + const dict = slice.loadDictDirect( + Dictionary.Keys.Uint(8), + Dictionary.Values.Cell() + ); + for (const [, value] of dict) { + const dataSource = parseDataSource(value); + if (dataSource) { + dataSources.push(dataSource); + } + } + return dataSources; +} + +export function parseDataSource(cell: Cell): DataSource | null { + const slice = cell.beginParse(); + if (slice.remainingBits === 0) { + return null; + } + const emitterChain = slice.loadUint(16); + const emitterAddress = slice.loadUintBig(256).toString(16).padStart(64, "0"); + return { emitterChain, emitterAddress }; +} + +export function parseGuardianSetKeys(cell: Cell): string[] { + const keys: string[] = []; + + function parseCell(c: Cell) { + let slice = c.beginParse(); + while (slice.remainingRefs > 0 || slice.remainingBits >= 160) { + if (slice.remainingBits >= 160) { + const bitsToSkip = slice.remainingBits - 160; + slice = slice.skip(bitsToSkip); + const key = slice.loadBits(160); + keys.push("0x" + key.toString()); + } + if (slice.remainingRefs > 0) { + parseCell(slice.loadRef()); + } + } + } + + parseCell(cell); + return keys; +}