@pythnetwork/pyth-sui-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2023 Pyth Contributors.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Pyth Sui JS SDK
2
+
3
+ [Pyth](https://pyth.network/) provides real-time pricing data in a variety of asset classes, including cryptocurrency, equities, FX and commodities. This library allows you to use these real-time prices on the [Sui network](https://sui.io/).
4
+
5
+ ## Installation
6
+
7
+ ### npm
8
+
9
+ ```
10
+ $ npm install --save @pythnetwork/pyth-sui-js
11
+ ```
12
+
13
+ ### Yarn
14
+
15
+ ```
16
+ $ yarn add @pythnetwork/pyth-sui-js
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ Pyth stores prices off-chain to minimize gas fees, which allows us to offer a wider selection of products and faster update times.
22
+ See [On-Demand Updates](https://docs.pyth.network/documentation/pythnet-price-feeds/on-demand) for more information about this approach.
23
+ Typically, to use Pyth prices on chain,
24
+ they must be fetched from an off-chain price service. The `SuiPriceServiceConnection` class can be used to interact with these services,
25
+ providing a way to fetch these prices directly in your code. The following example wraps an existing RPC provider and shows how to obtain
26
+ Pyth prices and submit them to the network:
27
+
28
+ ```typescript
29
+ const connection = new SuiPriceServiceConnection(
30
+ "https://hermes-beta.pyth.network"
31
+ ); // See Price Service endpoints section below for other endpoints
32
+
33
+ const priceIds = [
34
+ // You can find the ids of prices at https://pyth.network/developers/price-feed-ids#sui-testnet
35
+ "0xf9c0172ba10dfa4d19088d94f5bf61d3b54d5bd7483a322a982e1373ee8ea31b", // BTC/USD price id in testnet
36
+ "0xca80ba6dc32e08d06f1aa886011eed1d77c77be9eb761cc10d72b7d0a2fd57a6", // ETH/USD price id in testnet
37
+ ];
38
+
39
+ // In order to use Pyth prices in your protocol you need to submit the price update data to Pyth contract in your target
40
+ // chain. `getPriceUpdateData` creates the update data which can be submitted to your contract.
41
+
42
+ const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIds);
43
+ ```
44
+
45
+ ## On-chain prices
46
+
47
+ ### **_Important Note for Integrators_**
48
+
49
+ Your Sui Move module **should NOT** have a hard-coded call to `pyth::update_single_price_feed`. In other words, the Sui Pyth `pyth::update_single_price_feed` entry point should never be called by a contract, instead it should be called directly from client code (e.g. Typescript or Rust).
50
+
51
+ This is because when a Sui contract is [upgraded](https://docs.sui.io/build/package-upgrades), the new address is different from the original. If your module has a hard-coded call to `pyth::update_single_price_feed` living at a fixed call-site, it may eventually get bricked due to the way Pyth upgrades are implemented. (We only allows users to interact with the most recent package version for security reasons).
52
+
53
+ Therefore, you should build a [Sui programmable transaction](https://docs.sui.io/build/prog-trans-ts-sdk) that first updates the price by calling `pyth::update_single_price_feed` at the latest call-site from the client-side and then call a function in your contract that invokes `pyth::get_price` on the `PriceInfoObject` to get the recently updated price.
54
+ You can use `SuiPythClient` to build such transactions.
55
+
56
+ ### Example
57
+
58
+ ```ts
59
+ import { SuiPythClient } from "@pythnetwork/pyth-sui-js";
60
+ import { TransactionBlock } from "@mysten/sui.js";
61
+
62
+ const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIds); // see quickstart section
63
+
64
+
65
+ // It is either injected from browser or instantiated in backend via some private key
66
+ const wallet: SignerWithProvider = getWallet();
67
+ // Get the state ids of the Pyth and Wormhole contracts from
68
+ // https://docs.pyth.network/documentation/pythnet-price-feeds/sui
69
+ const wormholeStateId = " 0xFILL_ME";
70
+ const pythStateId = "0xFILL_ME";
71
+
72
+ const client = new SuiPythClient(wallet.provider, pythStateId, wormholeStateId);
73
+ const tx = new TransactionBlock();
74
+ const priceInfoObjectIds = await client.updatePriceFeeds(tx, priceFeedUpdateData, priceIds);
75
+
76
+ tx.moveCall({
77
+ target: `YOUR_PACKAGE::YOUR_MODULE::use_pyth_for_defi`,
78
+ arguments: [
79
+ ..., // other arguments needed for your contract
80
+ tx.object(pythStateId),
81
+ tx.object(priceInfoObjectIds[0]),
82
+ ],
83
+ });
84
+
85
+ const txBlock = {
86
+ transactionBlock: tx,
87
+ options: {
88
+ showEffects: true,
89
+ showEvents: true,
90
+ },
91
+ };
92
+
93
+ const result = await wallet.signAndExecuteTransactionBlock(txBlock);
94
+ ```
95
+
96
+ Now in your contract you can consume the price by calling `pyth::get_price` or other utility functions on the `PriceInfoObject`.
97
+
98
+ ### CLI Example
99
+
100
+ [This example](./src/examples/SuiRelay.ts) shows how to update prices on an Sui network. It does the following:
101
+
102
+ 1. Fetches update data from the Price Service for the given price feeds.
103
+ 2. Calls the Pyth Sui contract with the update data.
104
+
105
+ You can run this example with `npm run example-relay`. A full command that updates prices on Sui testnet looks like:
106
+
107
+ ```bash
108
+ export SUI_KEY=YOUR_PRIV_KEY;
109
+ npm run example-relay -- --feed-id "5a035d5440f5c163069af66062bac6c79377bf88396fa27e6067bfca8096d280" \
110
+ --price-service "https://hermes-beta.pyth.network" \
111
+ --full-node "https://fullnode.testnet.sui.io:443" \
112
+ --pyth-state-id "0xd3e79c2c083b934e78b3bd58a490ec6b092561954da6e7322e1e2b3c8abfddc0" \
113
+ --wormhole-state-id "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790"
114
+ ```
115
+
116
+ ## Off-chain prices
117
+
118
+ Many applications additionally need to display Pyth prices off-chain, for example, in their frontend application.
119
+ The `SuiPriceServiceConnection` provides two different ways to fetch the current Pyth price.
120
+ The code blocks below assume that the `connection` and `priceIds` objects have been initialized as shown above.
121
+ The first method is a single-shot query:
122
+
123
+ ```typescript
124
+ // `getLatestPriceFeeds` returns a `PriceFeed` for each price id. It contains all information about a price and has
125
+ // utility functions to get the current and exponentially-weighted moving average price, and other functionality.
126
+ const priceFeeds = await connection.getLatestPriceFeeds(priceIds);
127
+ // Get the price if it is not older than 60 seconds from the current time.
128
+ console.log(priceFeeds[0].getPriceNoOlderThan(60)); // Price { conf: '1234', expo: -8, price: '12345678' }
129
+ // Get the exponentially-weighted moving average price if it is not older than 60 seconds from the current time.
130
+ console.log(priceFeeds[1].getEmaPriceNoOlderThan(60));
131
+ ```
132
+
133
+ The object also supports a streaming websocket connection that allows you to subscribe to every new price update for a given feed.
134
+ This method is useful if you want to show continuously updating real-time prices in your frontend:
135
+
136
+ ```typescript
137
+ // Subscribe to the price feeds given by `priceId`. The callback will be invoked every time the requested feed
138
+ // gets a price update.
139
+ connection.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
140
+ console.log(
141
+ `Received update for ${priceFeed.id}: ${priceFeed.getPriceNoOlderThan(60)}`
142
+ );
143
+ });
144
+
145
+ // When using the subscription, make sure to close the websocket upon termination to finish the process gracefully.
146
+ setTimeout(() => {
147
+ connection.closeWebSocket();
148
+ }, 60000);
149
+ ```
150
+
151
+ ## [Price Service endpoints](https://docs.pyth.network/documentation/pythnet-price-feeds/price-service#public-endpoints)
@@ -0,0 +1,15 @@
1
+ /// <reference types="node" />
2
+ import { PriceServiceConnection, HexString } from "@pythnetwork/price-service-client";
3
+ import { Buffer } from "buffer";
4
+ export declare class SuiPriceServiceConnection extends PriceServiceConnection {
5
+ /**
6
+ * Gets price update data (either batch price attestation VAAs or accumulator messages, depending on the chosen endpoint), which then
7
+ * can be submitted to the Pyth contract to update the prices. This will throw an axios error if there is a network problem or
8
+ * the price service returns a non-ok response (e.g: Invalid price ids)
9
+ *
10
+ * @param priceIds Array of hex-encoded price ids.
11
+ * @returns Array of buffers containing the price update data.
12
+ */
13
+ getPriceFeedsUpdateData(priceIds: HexString[]): Promise<Buffer[]>;
14
+ }
15
+ //# sourceMappingURL=SuiPriceServiceConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SuiPriceServiceConnection.d.ts","sourceRoot":"","sources":["../src/SuiPriceServiceConnection.ts"],"names":[],"mappings":";AAAA,OAAO,EACL,sBAAsB,EACtB,SAAS,EACV,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,qBAAa,yBAA0B,SAAQ,sBAAsB;IACnE;;;;;;;OAOG;IACG,uBAAuB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAKxE"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SuiPriceServiceConnection = void 0;
4
+ const price_service_client_1 = require("@pythnetwork/price-service-client");
5
+ const buffer_1 = require("buffer");
6
+ class SuiPriceServiceConnection extends price_service_client_1.PriceServiceConnection {
7
+ /**
8
+ * Gets price update data (either batch price attestation VAAs or accumulator messages, depending on the chosen endpoint), which then
9
+ * can be submitted to the Pyth contract to update the prices. This will throw an axios error if there is a network problem or
10
+ * the price service returns a non-ok response (e.g: Invalid price ids)
11
+ *
12
+ * @param priceIds Array of hex-encoded price ids.
13
+ * @returns Array of buffers containing the price update data.
14
+ */
15
+ async getPriceFeedsUpdateData(priceIds) {
16
+ // Fetch the latest price feed update VAAs from the price service
17
+ const latestVaas = await this.getLatestVaas(priceIds);
18
+ return latestVaas.map((vaa) => buffer_1.Buffer.from(vaa, "base64"));
19
+ }
20
+ }
21
+ exports.SuiPriceServiceConnection = SuiPriceServiceConnection;
@@ -0,0 +1,80 @@
1
+ /// <reference types="node" />
2
+ import { JsonRpcProvider, ObjectId, TransactionBlock } from "@mysten/sui.js";
3
+ import { HexString } from "@pythnetwork/price-service-client";
4
+ export declare class SuiPythClient {
5
+ provider: JsonRpcProvider;
6
+ pythStateId: ObjectId;
7
+ wormholeStateId: ObjectId;
8
+ private pythPackageId;
9
+ private wormholePackageId;
10
+ private priceTableId;
11
+ private priceFeedObjectIdCache;
12
+ private baseUpdateFee;
13
+ constructor(provider: JsonRpcProvider, pythStateId: ObjectId, wormholeStateId: ObjectId);
14
+ getBaseUpdateFee(): Promise<number>;
15
+ /**
16
+ * getPackageId returns the latest package id that the object belongs to. Use this to
17
+ * fetch the latest package id for a given object id and handle package upgrades automatically.
18
+ * @param objectId
19
+ * @returns package id
20
+ */
21
+ getPackageId(objectId: ObjectId): Promise<ObjectId>;
22
+ /**
23
+ * Adds the commands for calling wormhole and verifying the vaas and returns the verified vaas.
24
+ * @param vaas array of vaas to verify
25
+ * @param tx transaction block to add commands to
26
+ */
27
+ verifyVaas(vaas: Buffer[], tx: TransactionBlock): Promise<({
28
+ kind: "Input";
29
+ index: number;
30
+ type?: "object" | "pure" | undefined;
31
+ value?: any;
32
+ } | {
33
+ kind: "GasCoin";
34
+ } | {
35
+ kind: "Result";
36
+ index: number;
37
+ } | {
38
+ kind: "NestedResult";
39
+ index: number;
40
+ resultIndex: number;
41
+ })[]>;
42
+ /**
43
+ * Adds the necessary commands for updating the pyth price feeds to the transaction block.
44
+ * @param tx transaction block to add commands to
45
+ * @param updates array of price feed updates received from the price service
46
+ * @param feedIds array of feed ids to update (in hex format)
47
+ */
48
+ updatePriceFeeds(tx: TransactionBlock, updates: Buffer[], feedIds: HexString[]): Promise<ObjectId[]>;
49
+ createPriceFeed(tx: TransactionBlock, updates: Buffer[]): Promise<void>;
50
+ /**
51
+ * Get the packageId for the wormhole package if not already cached
52
+ */
53
+ getWormholePackageId(): Promise<string>;
54
+ /**
55
+ * Get the packageId for the pyth package if not already cached
56
+ */
57
+ getPythPackageId(): Promise<string>;
58
+ /**
59
+ * Get the priceFeedObjectId for a given feedId if not already cached
60
+ * @param feedId
61
+ */
62
+ getPriceFeedObjectId(feedId: HexString): Promise<ObjectId | undefined>;
63
+ /**
64
+ * Fetches the price table object id for the current state id if not cached
65
+ * @returns price table object id
66
+ */
67
+ getPriceTableId(): Promise<ObjectId>;
68
+ /**
69
+ * Checks if a message is an accumulator message or not
70
+ * @param msg - update message from price service
71
+ */
72
+ isAccumulatorMsg(msg: Buffer): boolean;
73
+ /**
74
+ * Obtains the vaa bytes embedded in an accumulator message.
75
+ * @param accumulatorMessage - the accumulator price update message
76
+ * @returns vaa bytes as a uint8 array
77
+ */
78
+ extractVaaBytesFromAccumulatorMessage(accumulatorMessage: Buffer): Buffer;
79
+ }
80
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";AAAA,OAAO,EACL,eAAe,EACf,QAAQ,EAER,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAE9D,qBAAa,aAAa;IAOf,QAAQ,EAAE,eAAe;IACzB,WAAW,EAAE,QAAQ;IACrB,eAAe,EAAE,QAAQ;IARlC,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,iBAAiB,CAAuB;IAChD,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,sBAAsB,CAAuC;IACrE,OAAO,CAAC,aAAa,CAAqB;gBAEjC,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,QAAQ,EACrB,eAAe,EAAE,QAAQ;IAM5B,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;IAkBzC;;;;;OAKG;IACG,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAuBzD;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,gBAAgB;;;;;;;;;;;;;;;IAiBrD;;;;;OAKG;IACG,gBAAgB,CACpB,EAAE,EAAE,gBAAgB,EACpB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,SAAS,EAAE,GACnB,OAAO,CAAC,QAAQ,EAAE,CAAC;IAqEhB,eAAe,CAAC,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE;IAsC7D;;OAEG;IACG,oBAAoB;IAO1B;;OAEG;IACG,gBAAgB;IAOtB;;;OAGG;IACG,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IA2B5E;;;OAGG;IACG,eAAe,IAAI,OAAO,CAAC,QAAQ,CAAC;IAmB1C;;;OAGG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM;IAK5B;;;;OAIG;IACH,qCAAqC,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM;CAe1E"}
package/lib/client.js ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SuiPythClient = void 0;
4
+ const sui_js_1 = require("@mysten/sui.js");
5
+ class SuiPythClient {
6
+ provider;
7
+ pythStateId;
8
+ wormholeStateId;
9
+ pythPackageId;
10
+ wormholePackageId;
11
+ priceTableId;
12
+ priceFeedObjectIdCache = new Map();
13
+ baseUpdateFee;
14
+ constructor(provider, pythStateId, wormholeStateId) {
15
+ this.provider = provider;
16
+ this.pythStateId = pythStateId;
17
+ this.wormholeStateId = wormholeStateId;
18
+ this.pythPackageId = undefined;
19
+ this.wormholePackageId = undefined;
20
+ }
21
+ async getBaseUpdateFee() {
22
+ if (this.baseUpdateFee === undefined) {
23
+ const result = await this.provider.getObject({
24
+ id: this.pythStateId,
25
+ options: { showContent: true },
26
+ });
27
+ if (!result.data ||
28
+ !result.data.content ||
29
+ result.data.content.dataType !== "moveObject")
30
+ throw new Error("Unable to fetch pyth state object");
31
+ this.baseUpdateFee = result.data.content.fields.base_update_fee;
32
+ }
33
+ return this.baseUpdateFee;
34
+ }
35
+ /**
36
+ * getPackageId returns the latest package id that the object belongs to. Use this to
37
+ * fetch the latest package id for a given object id and handle package upgrades automatically.
38
+ * @param objectId
39
+ * @returns package id
40
+ */
41
+ async getPackageId(objectId) {
42
+ const state = await this.provider
43
+ .getObject({
44
+ id: objectId,
45
+ options: {
46
+ showContent: true,
47
+ },
48
+ })
49
+ .then((result) => {
50
+ if (result.data?.content?.dataType == "moveObject") {
51
+ return result.data.content.fields;
52
+ }
53
+ throw new Error("not move object");
54
+ });
55
+ if ("upgrade_cap" in state) {
56
+ return state.upgrade_cap.fields.package;
57
+ }
58
+ throw new Error("upgrade_cap not found");
59
+ }
60
+ /**
61
+ * Adds the commands for calling wormhole and verifying the vaas and returns the verified vaas.
62
+ * @param vaas array of vaas to verify
63
+ * @param tx transaction block to add commands to
64
+ */
65
+ async verifyVaas(vaas, tx) {
66
+ const wormholePackageId = await this.getWormholePackageId();
67
+ const verifiedVaas = [];
68
+ for (const vaa of vaas) {
69
+ const [verifiedVaa] = tx.moveCall({
70
+ target: `${wormholePackageId}::vaa::parse_and_verify`,
71
+ arguments: [
72
+ tx.object(this.wormholeStateId),
73
+ tx.pure(Array.from(vaa)),
74
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
75
+ ],
76
+ });
77
+ verifiedVaas.push(verifiedVaa);
78
+ }
79
+ return verifiedVaas;
80
+ }
81
+ /**
82
+ * Adds the necessary commands for updating the pyth price feeds to the transaction block.
83
+ * @param tx transaction block to add commands to
84
+ * @param updates array of price feed updates received from the price service
85
+ * @param feedIds array of feed ids to update (in hex format)
86
+ */
87
+ async updatePriceFeeds(tx, updates, feedIds) {
88
+ const wormholePackageId = await this.getWormholePackageId();
89
+ const packageId = await this.getPythPackageId();
90
+ let priceUpdatesHotPotato;
91
+ if (updates.every((update) => this.isAccumulatorMsg(update))) {
92
+ if (updates.length > 1) {
93
+ throw new Error("SDK does not support sending multiple accumulator messages in a single transaction");
94
+ }
95
+ const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]);
96
+ const verifiedVaas = await this.verifyVaas([vaa], tx);
97
+ [priceUpdatesHotPotato] = tx.moveCall({
98
+ target: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`,
99
+ arguments: [
100
+ tx.object(this.pythStateId),
101
+ tx.pure(Array.from(updates[0])),
102
+ verifiedVaas[0],
103
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
104
+ ],
105
+ });
106
+ }
107
+ else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
108
+ const verifiedVaas = await this.verifyVaas(updates, tx);
109
+ [priceUpdatesHotPotato] = tx.moveCall({
110
+ target: `${packageId}::pyth::create_price_infos_hot_potato`,
111
+ arguments: [
112
+ tx.object(this.pythStateId),
113
+ tx.makeMoveVec({
114
+ type: `${wormholePackageId}::vaa::VAA`,
115
+ objects: verifiedVaas,
116
+ }),
117
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
118
+ ],
119
+ });
120
+ }
121
+ else {
122
+ throw new Error("Can't mix accumulator and non-accumulator messages");
123
+ }
124
+ const priceInfoObjects = [];
125
+ for (const feedId of feedIds) {
126
+ const priceInfoObjectId = await this.getPriceFeedObjectId(feedId);
127
+ if (!priceInfoObjectId) {
128
+ throw new Error(`Price feed ${feedId} not found, please create it first`);
129
+ }
130
+ priceInfoObjects.push(priceInfoObjectId);
131
+ const coin = tx.splitCoins(tx.gas, [
132
+ tx.pure(await this.getBaseUpdateFee()),
133
+ ]);
134
+ [priceUpdatesHotPotato] = tx.moveCall({
135
+ target: `${packageId}::pyth::update_single_price_feed`,
136
+ arguments: [
137
+ tx.object(this.pythStateId),
138
+ priceUpdatesHotPotato,
139
+ tx.object(priceInfoObjectId),
140
+ coin,
141
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
142
+ ],
143
+ });
144
+ }
145
+ tx.moveCall({
146
+ target: `${packageId}::hot_potato_vector::destroy`,
147
+ arguments: [priceUpdatesHotPotato],
148
+ typeArguments: [`${packageId}::price_info::PriceInfo`],
149
+ });
150
+ return priceInfoObjects;
151
+ }
152
+ async createPriceFeed(tx, updates) {
153
+ const wormholePackageId = await this.getWormholePackageId();
154
+ const packageId = await this.getPythPackageId();
155
+ if (updates.every((update) => this.isAccumulatorMsg(update))) {
156
+ if (updates.length > 1) {
157
+ throw new Error("SDK does not support sending multiple accumulator messages in a single transaction");
158
+ }
159
+ const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]);
160
+ const verifiedVaas = await this.verifyVaas([vaa], tx);
161
+ tx.moveCall({
162
+ target: `${packageId}::pyth::create_price_feeds_using_accumulator`,
163
+ arguments: [
164
+ tx.object(this.pythStateId),
165
+ tx.pure(Array.from(updates[0])),
166
+ verifiedVaas[0],
167
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
168
+ ],
169
+ });
170
+ }
171
+ else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
172
+ const verifiedVaas = await this.verifyVaas(updates, tx);
173
+ tx.moveCall({
174
+ target: `${packageId}::pyth::create_price_feeds`,
175
+ arguments: [
176
+ tx.object(this.pythStateId),
177
+ tx.makeMoveVec({
178
+ type: `${wormholePackageId}::vaa::VAA`,
179
+ objects: verifiedVaas,
180
+ }),
181
+ tx.object(sui_js_1.SUI_CLOCK_OBJECT_ID),
182
+ ],
183
+ });
184
+ }
185
+ else {
186
+ throw new Error("Can't mix accumulator and non-accumulator messages");
187
+ }
188
+ }
189
+ /**
190
+ * Get the packageId for the wormhole package if not already cached
191
+ */
192
+ async getWormholePackageId() {
193
+ if (!this.wormholePackageId) {
194
+ this.wormholePackageId = await this.getPackageId(this.wormholeStateId);
195
+ }
196
+ return this.wormholePackageId;
197
+ }
198
+ /**
199
+ * Get the packageId for the pyth package if not already cached
200
+ */
201
+ async getPythPackageId() {
202
+ if (!this.pythPackageId) {
203
+ this.pythPackageId = await this.getPackageId(this.pythStateId);
204
+ }
205
+ return this.pythPackageId;
206
+ }
207
+ /**
208
+ * Get the priceFeedObjectId for a given feedId if not already cached
209
+ * @param feedId
210
+ */
211
+ async getPriceFeedObjectId(feedId) {
212
+ const normalizedFeedId = feedId.replace("0x", "");
213
+ if (!this.priceFeedObjectIdCache.has(normalizedFeedId)) {
214
+ const tableId = await this.getPriceTableId();
215
+ const result = await this.provider.getDynamicFieldObject({
216
+ parentId: tableId,
217
+ name: {
218
+ type: `${await this.getPythPackageId()}::price_identifier::PriceIdentifier`,
219
+ value: {
220
+ bytes: Array.from(Buffer.from(normalizedFeedId, "hex")),
221
+ },
222
+ },
223
+ });
224
+ if (!result.data || !result.data.content) {
225
+ return undefined;
226
+ }
227
+ if (result.data.content.dataType !== "moveObject") {
228
+ throw new Error("Price feed type mismatch");
229
+ }
230
+ this.priceFeedObjectIdCache.set(normalizedFeedId, result.data.content.fields.value);
231
+ }
232
+ return this.priceFeedObjectIdCache.get(normalizedFeedId);
233
+ }
234
+ /**
235
+ * Fetches the price table object id for the current state id if not cached
236
+ * @returns price table object id
237
+ */
238
+ async getPriceTableId() {
239
+ if (this.priceTableId === undefined) {
240
+ const result = await this.provider.getDynamicFieldObject({
241
+ parentId: this.pythStateId,
242
+ name: {
243
+ type: "vector<u8>",
244
+ value: "price_info",
245
+ },
246
+ });
247
+ if (!result.data) {
248
+ throw new Error("Price Table not found, contract may not be initialized");
249
+ }
250
+ this.priceTableId = result.data.objectId;
251
+ }
252
+ return this.priceTableId;
253
+ }
254
+ /**
255
+ * Checks if a message is an accumulator message or not
256
+ * @param msg - update message from price service
257
+ */
258
+ isAccumulatorMsg(msg) {
259
+ const ACCUMULATOR_MAGIC = "504e4155";
260
+ return msg.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC;
261
+ }
262
+ /**
263
+ * Obtains the vaa bytes embedded in an accumulator message.
264
+ * @param accumulatorMessage - the accumulator price update message
265
+ * @returns vaa bytes as a uint8 array
266
+ */
267
+ extractVaaBytesFromAccumulatorMessage(accumulatorMessage) {
268
+ if (!this.isAccumulatorMsg(accumulatorMessage)) {
269
+ throw new Error("Not an accumulator message");
270
+ }
271
+ // the first 6 bytes in the accumulator message encode the header, major, and minor bytes
272
+ // we ignore them, since we are only interested in the VAA bytes
273
+ const trailingPayloadSize = accumulatorMessage.readUint8(6);
274
+ const vaaSizeOffset = 7 + // header bytes (header(4) + major(1) + minor(1) + trailing payload size(1))
275
+ trailingPayloadSize + // trailing payload (variable number of bytes)
276
+ 1; // proof_type (1 byte)
277
+ const vaaSize = accumulatorMessage.readUint16BE(vaaSizeOffset);
278
+ const vaaOffset = vaaSizeOffset + 2;
279
+ return accumulatorMessage.subarray(vaaOffset, vaaOffset + vaaSize);
280
+ }
281
+ }
282
+ exports.SuiPythClient = SuiPythClient;
@@ -0,0 +1,3 @@
1
+ import { JsonRpcProvider } from "@mysten/sui.js";
2
+ export declare function getProvider(url: string): JsonRpcProvider;
3
+ //# sourceMappingURL=SuiRelay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SuiRelay.d.ts","sourceRoot":"","sources":["../../src/examples/SuiRelay.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,eAAe,EAGhB,MAAM,gBAAgB,CAAC;AAmCxB,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,mBAEtC"}
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getProvider = void 0;
7
+ const yargs_1 = __importDefault(require("yargs"));
8
+ const helpers_1 = require("yargs/helpers");
9
+ const sui_js_1 = require("@mysten/sui.js");
10
+ const client_1 = require("../client");
11
+ const index_1 = require("../index");
12
+ const argvPromise = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
13
+ .option("feed-id", {
14
+ description: "Price feed ids to update without the leading 0x (e.g f9c0172ba10dfa4d19088d94f5bf61d3b54d5bd7483a322a982e1373ee8ea31b). Can be provided multiple times for multiple feed updates",
15
+ type: "array",
16
+ demandOption: true,
17
+ })
18
+ .option("price-service", {
19
+ description: "Endpoint URL for the price service. e.g: https://xc-mainnet.pyth.network",
20
+ type: "string",
21
+ demandOption: true,
22
+ })
23
+ .option("full-node", {
24
+ description: "URL of the full Sui node RPC endpoint. e.g: https://fullnode.testnet.sui.io:443",
25
+ type: "string",
26
+ demandOption: true,
27
+ })
28
+ .option("pyth-state-id", {
29
+ description: "Pyth state object id.",
30
+ type: "string",
31
+ demandOption: true,
32
+ })
33
+ .option("wormhole-state-id", {
34
+ description: "Wormhole state object id.",
35
+ type: "string",
36
+ demandOption: true,
37
+ }).argv;
38
+ function getProvider(url) {
39
+ return new sui_js_1.JsonRpcProvider(new sui_js_1.Connection({ fullnode: url }));
40
+ }
41
+ exports.getProvider = getProvider;
42
+ async function run() {
43
+ if (process.env.SUI_KEY === undefined) {
44
+ throw new Error(`SUI_KEY environment variable should be set.`);
45
+ }
46
+ const argv = await argvPromise;
47
+ // Fetch the latest price feed update data from the Price Service
48
+ const connection = new index_1.SuiPriceServiceConnection(argv["price-service"]);
49
+ const feeds = argv["feed-id"];
50
+ const priceFeedUpdateData = await connection.getPriceFeedsUpdateData(feeds);
51
+ const provider = getProvider(argv["full-node"]);
52
+ const wormholeStateId = argv["wormhole-state-id"];
53
+ const pythStateId = argv["pyth-state-id"];
54
+ const client = new client_1.SuiPythClient(provider, pythStateId, wormholeStateId);
55
+ const tx = new sui_js_1.TransactionBlock();
56
+ await client.updatePriceFeeds(tx, priceFeedUpdateData, feeds);
57
+ const wallet = new sui_js_1.RawSigner(sui_js_1.Ed25519Keypair.fromSecretKey(Buffer.from(process.env.SUI_KEY, "hex")), provider);
58
+ const txBlock = {
59
+ transactionBlock: tx,
60
+ options: {
61
+ showEffects: true,
62
+ showEvents: true,
63
+ },
64
+ };
65
+ const result = await wallet.signAndExecuteTransactionBlock(txBlock);
66
+ console.dir(result, { depth: null });
67
+ }
68
+ run();
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { SuiPriceServiceConnection } from "./SuiPriceServiceConnection";
2
+ export { SuiPythClient } from "./client";
3
+ export { DurationInMs, HexString, Price, PriceFeed, PriceServiceConnectionConfig, UnixTimestamp, } from "@pythnetwork/price-service-client";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EACL,YAAY,EACZ,SAAS,EACT,KAAK,EACL,SAAS,EACT,4BAA4B,EAC5B,aAAa,GACd,MAAM,mCAAmC,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PriceFeed = exports.Price = exports.SuiPythClient = exports.SuiPriceServiceConnection = void 0;
4
+ var SuiPriceServiceConnection_1 = require("./SuiPriceServiceConnection");
5
+ Object.defineProperty(exports, "SuiPriceServiceConnection", { enumerable: true, get: function () { return SuiPriceServiceConnection_1.SuiPriceServiceConnection; } });
6
+ var client_1 = require("./client");
7
+ Object.defineProperty(exports, "SuiPythClient", { enumerable: true, get: function () { return client_1.SuiPythClient; } });
8
+ var price_service_client_1 = require("@pythnetwork/price-service-client");
9
+ Object.defineProperty(exports, "Price", { enumerable: true, get: function () { return price_service_client_1.Price; } });
10
+ Object.defineProperty(exports, "PriceFeed", { enumerable: true, get: function () { return price_service_client_1.PriceFeed; } });
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@pythnetwork/pyth-sui-js",
3
+ "version": "1.0.0",
4
+ "description": "Pyth Network Sui Utilities",
5
+ "homepage": "https://pyth.network",
6
+ "author": {
7
+ "name": "Pyth Data Association"
8
+ },
9
+ "main": "lib/index.js",
10
+ "types": "lib/index.d.ts",
11
+ "files": [
12
+ "lib/**/*"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/pyth-network/pyth-crosschain",
17
+ "directory": "target_chains/sui/sdk/js"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "test": "jest --passWithNoTests",
24
+ "build": "tsc",
25
+ "example-relay": "npm run build && node lib/examples/SuiRelay.js",
26
+ "format": "prettier --write \"src/**/*.ts\"",
27
+ "lint": "eslint src/",
28
+ "prepublishOnly": "npm run build && npm test && npm run lint",
29
+ "preversion": "npm run lint",
30
+ "version": "npm run format && git add -A src"
31
+ },
32
+ "keywords": [
33
+ "pyth",
34
+ "oracle",
35
+ "sui"
36
+ ],
37
+ "license": "Apache-2.0",
38
+ "devDependencies": {
39
+ "@truffle/hdwallet-provider": "^2.1.5",
40
+ "@types/ethereum-protocol": "^1.0.2",
41
+ "@types/jest": "^29.4.0",
42
+ "@types/node": "^18.11.18",
43
+ "@types/web3-provider-engine": "^14.0.1",
44
+ "@types/yargs": "^17.0.20",
45
+ "@typescript-eslint/eslint-plugin": "^5.21.0",
46
+ "@typescript-eslint/parser": "^5.21.0",
47
+ "eslint": "^8.14.0",
48
+ "jest": "^29.4.1",
49
+ "prettier": "^2.6.2",
50
+ "ts-jest": "^29.0.5",
51
+ "typescript": "^4.6.3",
52
+ "web3": "^1.8.2",
53
+ "yargs": "^17.0.20"
54
+ },
55
+ "dependencies": {
56
+ "@mysten/sui.js": "^0.32.2",
57
+ "@pythnetwork/price-service-client": "*",
58
+ "buffer": "^6.0.3"
59
+ },
60
+ "gitHead": "55129e5b891b0ce0271dbee69a8b4b7512a222d1"
61
+ }