diff --git a/contract_manager/src/chains.ts b/contract_manager/src/chains.ts index 9dbc92c006..189765eb81 100644 --- a/contract_manager/src/chains.ts +++ b/contract_manager/src/chains.ts @@ -31,8 +31,11 @@ import { WalletContractV4, ContractProvider, Address, + OpenedContract, + Sender, } from "@ton/ton"; -import { keyPairFromSecretKey } from "@ton/crypto"; +import { keyPairFromSeed } from "@ton/crypto"; +import { PythContract } from "@pythnetwork/pyth-ton-js"; export type ChainConfig = Record & { mainnet: boolean; @@ -759,21 +762,44 @@ export class TonChain extends Chain { super(id, mainnet, wormholeChainName, nativeToken); } - async getProvider(address: string): Promise { + 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 = keyPairFromSecretKey(Buffer.from(privateKey, "hex")); - const wallet = WalletContractV4.create({ + const keyPair = keyPairFromSeed(Buffer.from(privateKey, "hex")); + return WalletContractV4.create({ publicKey: keyPair.publicKey, workchain: 0, }); + } - return wallet; + 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); } /** @@ -817,7 +843,7 @@ export class TonChain extends Chain { async getAccountBalance(privateKey: PrivateKey): Promise { const wallet = await this.getWallet(privateKey); - const provider = await this.getProvider(wallet.address.toString()); + 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/ton.ts b/contract_manager/src/contracts/ton.ts index ba8e5af5b6..8af1a1d6e4 100644 --- a/contract_manager/src/contracts/ton.ts +++ b/contract_manager/src/contracts/ton.ts @@ -3,17 +3,8 @@ import { WormholeContract } from "./wormhole"; import { PriceFeed, PriceFeedContract, PrivateKey, TxResult } from "../base"; import { TokenQty } from "../token"; import { DataSource } from "@pythnetwork/xc-admin-common"; -import { - Address, - Contract, - OpenedContract, - TonClient, - WalletContractV4, -} from "@ton/ton"; -import { - PythContract, - PYTH_CONTRACT_ADDRESS_TESTNET, -} from "@pythnetwork/pyth-ton-js"; +import { Address, OpenedContract } from "@ton/ton"; +import { PythContract } from "@pythnetwork/pyth-ton-js"; export class TonWormholeContract extends WormholeContract { static type = "TonWormholeContract"; @@ -22,6 +13,10 @@ export class TonWormholeContract extends WormholeContract { return `${this.chain.getId()}_${this.address}_${TonWormholeContract.type}`; } + getChain(): TonChain { + return this.chain; + } + getType(): string { return TonWormholeContract.type; } @@ -52,45 +47,54 @@ export class TonWormholeContract extends WormholeContract { 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.get("get_current_guardian_set_index"); - // return Number(result); - return 1; + const contract = await this.getContract(); + const result = await contract.getCurrentGuardianSetIndex(); + return result; } async getChainId(): Promise { - // const contract = await this.getContract(); - // const result = await contract.get("get_chain_id"); - // return Number(result); - return 1; + 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.get("get_guardian_set", [guardianSetIndex]); - // return result.map((guardian: string) => `0x${guardian}`); - return ["0x1"]; + 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 client = await this.chain.getClient(senderPrivateKey); - // const contract = await this.getContract(client); - - // const tx = await contract.sendMessage({ - // body: { - // op: "update_guardian_set", - // data: vaa, - // }, - // value: "0.05", // TON to attach - // }); + 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: "0x1", + id: transactions[0].hash.toString(), info: JSON.stringify("0x1"), }; } @@ -118,12 +122,16 @@ export class TonPriceFeedContract extends PriceFeedContract { 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.getProvider(this.address); + const provider = await this.chain.getContractProvider(this.address); const contract = provider.open( PythContract.createFromAddress(Address.parse(this.address)) ); @@ -132,19 +140,18 @@ export class TonPriceFeedContract extends PriceFeedContract { } async getTotalFee(): Promise { - // const contract = await this.getContract(); - // const balance = await contract.getBalance(); + const client = await this.chain.getClient(); + const balance = await client.getBalance(Address.parse(this.address)); return { - amount: BigInt(0), + amount: balance, denom: this.chain.getNativeToken(), }; } async getLastExecutedGovernanceSequence(): Promise { - // const contract = await this.getContract(); - // const result = await contract.get - // return Number(result); - return 1; + const contract = await this.getContract(); + const result = await contract.getLastExecutedGovernanceSequence(); + return Number(result); } async getPriceFeed(feedId: string): Promise { @@ -174,8 +181,7 @@ export class TonPriceFeedContract extends PriceFeedContract { } async getValidTimePeriod(): Promise { - // const contract = await this.getContract(); - // const result = await contract.get("get_valid_time_period"); + // Not supported but return 1 because it's required by the abstract class return 1; } @@ -194,21 +200,23 @@ export class TonPriceFeedContract extends PriceFeedContract { } async getDataSources(): Promise { - // const contract = await this.getContract(); - // const result = await contract.get("get_data_sources"); - // return result.map((ds: any) => ({ - // emitterChain: ds.emitterChain, - // emitterAddress: ds.emitterAddress.replace("0x", ""), - // })); - return []; + 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.get("get_governance_data_source"); + const contract = await this.getContract(); + const result = await contract.getGovernanceDataSource(); + if (result === null) { + throw new Error("Governance data source not found"); + } return { - emitterChain: 1, - emitterAddress: "0x1", + emitterChain: result.emitterChain, + emitterAddress: result.emitterAddress, }; } @@ -216,22 +224,31 @@ export class TonPriceFeedContract extends PriceFeedContract { senderPrivateKey: PrivateKey, vaas: Buffer[] ): Promise { - // const client = await this.chain.getClient(senderPrivateKey); - // const contract = await this.getContract(client); - - // const updateFee = await contract.get("get_update_fee", [vaas[0]]); + 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, + 156000000n + BigInt(fee) + ); // 156000000 = 390000 (estimated gas used for the transaction, this is defined in target_chains/ton/contracts/common/gas.fc as UPDATE_PRICE_FEEDS_GAS) * 400 (current settings in basechain are as follows: 1 unit of gas costs 400 nanotons) + } - // const tx = await contract.sendMessage({ - // body: { - // op: "update_price_feeds", - // data: vaas[0], // TON contract expects single VAA - // }, - // value: updateFee.toString(), - // }); + 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: "0x1", - info: JSON.stringify("0x1"), + id: txHash, + info: txInfo, }; } @@ -239,27 +256,26 @@ export class TonPriceFeedContract extends PriceFeedContract { senderPrivateKey: PrivateKey, vaa: Buffer ): Promise { - // const client = await this.chain.getClient(senderPrivateKey); - // const contract = await this.getContract(client); - - // const tx = await contract.sendMessage({ - // body: { - // op: "execute_governance_action", - // data: vaa, - // }, - // value: "0.05", // TON to attach - // }); + 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: "0x1", - info: JSON.stringify("0x1"), + id: txHash, + info: txInfo, }; } - getChain(): TonChain { - return this.chain; - } - toJson() { return { chain: this.chain.getId(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14f4097e2d..85e440e22f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32362,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..c49150b92d 100644 --- a/target_chains/ton/contracts/tests/PythTest.spec.ts +++ b/target_chains/ton/contracts/tests/PythTest.spec.ts @@ -21,7 +21,7 @@ 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, @@ -512,9 +512,7 @@ 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( @@ -534,8 +532,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 +542,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 () => { @@ -674,8 +670,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 +693,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 @@ -743,8 +737,7 @@ 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 }); @@ -982,4 +975,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..452d54d9a9 100644 --- a/target_chains/ton/contracts/wrappers/BaseWrapper.ts +++ b/target_chains/ton/contracts/wrappers/BaseWrapper.ts @@ -88,7 +88,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..56c2b4a410 100644 --- a/target_chains/ton/contracts/wrappers/PythTest.ts +++ b/target_chains/ton/contracts/wrappers/PythTest.ts @@ -9,7 +9,11 @@ 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 = { @@ -135,7 +139,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 +192,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/src/index.ts b/target_chains/ton/sdk/js/src/index.ts index 7d6fdfda5e..8410b3ec37 100644 --- a/target_chains/ton/sdk/js/src/index.ts +++ b/target_chains/ton/sdk/js/src/index.ts @@ -3,14 +3,21 @@ import { beginCell, Cell, Contract, + Dictionary, Sender, SendMode, + toNano, } from "@ton/core"; import { ContractProvider } from "@ton/ton"; export const PYTH_CONTRACT_ADDRESS_TESTNET = "EQDwGkJmcj7MMmWAHmhldnY-lAKI6hcTQ2tAEcapmwCnztQU"; +export interface DataSource { + emitterChain: number; + emitterAddress: string; +} + export class PythContract implements Contract { constructor( readonly address: Address, @@ -27,6 +34,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 +69,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 +181,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("test_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 +264,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; +}