[Sui 21/x] - pyth-sui-js SDK (#1004)

* Add sui js sdk

* Update dependencies and package lock

---------

Co-authored-by: Amin Moghaddam <amin@pyth.network>
This commit is contained in:
optke3 2023-08-15 04:52:16 -04:00 committed by GitHub
parent f36bd21f31
commit 55129e5b89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 14139 additions and 2589 deletions

372
package-lock.json generated
View File

@ -20,6 +20,7 @@
"target_chains/ethereum/sdk/js", "target_chains/ethereum/sdk/js",
"target_chains/ethereum/sdk/solidity", "target_chains/ethereum/sdk/solidity",
"target_chains/ethereum/examples/oracle_swap/app", "target_chains/ethereum/examples/oracle_swap/app",
"target_chains/sui/sdk/js",
"third_party/pyth/p2w-relay", "third_party/pyth/p2w-relay",
"wormhole_attester/sdk/js", "wormhole_attester/sdk/js",
"contract_manager" "contract_manager"
@ -12005,6 +12006,10 @@
"resolved": "target_chains/ethereum/sdk/solidity", "resolved": "target_chains/ethereum/sdk/solidity",
"link": true "link": true
}, },
"node_modules/@pythnetwork/pyth-sui-js": {
"resolved": "target_chains/sui/sdk/js",
"link": true
},
"node_modules/@pythnetwork/pyth-terra-js": { "node_modules/@pythnetwork/pyth-terra-js": {
"resolved": "target_chains/cosmwasm/sdk/js", "resolved": "target_chains/cosmwasm/sdk/js",
"link": true "link": true
@ -59657,6 +59662,221 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"target_chains/sui/sdk/js": {
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@mysten/sui.js": "^0.32.2",
"@pythnetwork/price-service-client": "*",
"buffer": "^6.0.3"
},
"devDependencies": {
"@truffle/hdwallet-provider": "^2.1.5",
"@types/ethereum-protocol": "^1.0.2",
"@types/jest": "^29.4.0",
"@types/node": "^18.11.18",
"@types/web3-provider-engine": "^14.0.1",
"@types/yargs": "^17.0.20",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"eslint": "^8.14.0",
"jest": "^29.4.1",
"prettier": "^2.6.2",
"ts-jest": "^29.0.5",
"typescript": "^4.6.3",
"web3": "^1.8.2",
"yargs": "^17.0.20"
}
},
"target_chains/sui/sdk/js/node_modules/@mysten/sui.js": {
"version": "0.32.2",
"resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.32.2.tgz",
"integrity": "sha512-/Hm4xkGolJhqj8FvQr7QSHDTlxIvL52mtbOao9f75YjrBh7y1Uh9kbJSY7xiTF1NY9sv6p5hUVlYRJuM0Hvn9A==",
"dependencies": {
"@mysten/bcs": "0.7.1",
"@noble/curves": "^1.0.0",
"@noble/hashes": "^1.3.0",
"@scure/bip32": "^1.3.0",
"@scure/bip39": "^1.2.0",
"@suchipi/femver": "^1.0.0",
"jayson": "^4.0.0",
"rpc-websockets": "^7.5.1",
"superstruct": "^1.0.3",
"tweetnacl": "^1.0.3"
},
"engines": {
"node": ">=16"
}
},
"target_chains/sui/sdk/js/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"target_chains/sui/sdk/js/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"target_chains/sui/sdk/js/node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"target_chains/sui/sdk/js/node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"target_chains/sui/sdk/js/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"target_chains/sui/sdk/js/node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"target_chains/sui/sdk/js/node_modules/jayson": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz",
"integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==",
"dependencies": {
"@types/connect": "^3.4.33",
"@types/node": "^12.12.54",
"@types/ws": "^7.4.4",
"commander": "^2.20.3",
"delay": "^5.0.0",
"es6-promisify": "^5.0.0",
"eyes": "^0.1.8",
"isomorphic-ws": "^4.0.1",
"json-stringify-safe": "^5.0.1",
"JSONStream": "^1.3.5",
"uuid": "^8.3.2",
"ws": "^7.4.5"
},
"bin": {
"jayson": "bin/jayson.js"
},
"engines": {
"node": ">=8"
}
},
"target_chains/sui/sdk/js/node_modules/jayson/node_modules/@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
},
"target_chains/sui/sdk/js/node_modules/superstruct": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz",
"integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==",
"engines": {
"node": ">=14.0.0"
}
},
"target_chains/sui/sdk/js/node_modules/ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"target_chains/sui/sdk/js/node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"target_chains/sui/sdk/js/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"third_party/pyth/p2w-relay": { "third_party/pyth/p2w-relay": {
"name": "pyth_relay", "name": "pyth_relay",
"version": "1.0.0", "version": "1.0.0",
@ -71429,6 +71649,158 @@
} }
} }
}, },
"@pythnetwork/pyth-sui-js": {
"version": "file:target_chains/sui/sdk/js",
"requires": {
"@mysten/sui.js": "^0.32.2",
"@pythnetwork/price-service-client": "*",
"@truffle/hdwallet-provider": "^2.1.5",
"@types/ethereum-protocol": "^1.0.2",
"@types/jest": "^29.4.0",
"@types/node": "^18.11.18",
"@types/web3-provider-engine": "^14.0.1",
"@types/yargs": "^17.0.20",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"buffer": "^6.0.3",
"eslint": "^8.14.0",
"jest": "^29.4.1",
"prettier": "^2.6.2",
"ts-jest": "^29.0.5",
"typescript": "^4.6.3",
"web3": "^1.8.2",
"yargs": "^17.0.20"
},
"dependencies": {
"@mysten/sui.js": {
"version": "0.32.2",
"resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.32.2.tgz",
"integrity": "sha512-/Hm4xkGolJhqj8FvQr7QSHDTlxIvL52mtbOao9f75YjrBh7y1Uh9kbJSY7xiTF1NY9sv6p5hUVlYRJuM0Hvn9A==",
"requires": {
"@mysten/bcs": "0.7.1",
"@noble/curves": "^1.0.0",
"@noble/hashes": "^1.3.0",
"@scure/bip32": "^1.3.0",
"@scure/bip39": "^1.2.0",
"@suchipi/femver": "^1.0.0",
"jayson": "^4.0.0",
"rpc-websockets": "^7.5.1",
"superstruct": "^1.0.3",
"tweetnacl": "^1.0.3"
}
},
"@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"requires": {
"@noble/hashes": "1.3.1"
}
},
"@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
},
"@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"requires": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
}
},
"@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"requires": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
}
},
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
}
},
"jayson": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz",
"integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==",
"requires": {
"@types/connect": "^3.4.33",
"@types/node": "^12.12.54",
"@types/ws": "^7.4.4",
"commander": "^2.20.3",
"delay": "^5.0.0",
"es6-promisify": "^5.0.0",
"eyes": "^0.1.8",
"isomorphic-ws": "^4.0.1",
"json-stringify-safe": "^5.0.1",
"JSONStream": "^1.3.5",
"uuid": "^8.3.2",
"ws": "^7.4.5"
},
"dependencies": {
"@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
}
}
},
"superstruct": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz",
"integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg=="
},
"ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"requires": {}
},
"yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"requires": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
}
},
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true
}
}
},
"@pythnetwork/pyth-terra-js": { "@pythnetwork/pyth-terra-js": {
"version": "file:target_chains/cosmwasm/sdk/js", "version": "file:target_chains/cosmwasm/sdk/js",
"requires": { "requires": {

View File

@ -15,6 +15,7 @@
"target_chains/ethereum/sdk/js", "target_chains/ethereum/sdk/js",
"target_chains/ethereum/sdk/solidity", "target_chains/ethereum/sdk/solidity",
"target_chains/ethereum/examples/oracle_swap/app", "target_chains/ethereum/examples/oracle_swap/app",
"target_chains/sui/sdk/js",
"third_party/pyth/p2w-relay", "third_party/pyth/p2w-relay",
"wormhole_attester/sdk/js", "wormhole_attester/sdk/js",
"contract_manager" "contract_manager"

10477
target_chains/sui/scripts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,6 @@
"devDependencies": { "devDependencies": {
"@types/chai": "^4.3.4", "@types/chai": "^4.3.4",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@types/node": "^18.16.3" "@types/node": "^18.17.4"
} }
} }

View File

@ -25,7 +25,7 @@ async function main() {
]; ];
const priceFeedVAAs = await connection.getLatestVaas(data); const priceFeedVAAs = await connection.getLatestVaas(data);
//console.log("number of VAAs: ", priceFeedVAAs.length) //console.log("number of VAAs: ", priceFeedVAAs.length)
console.log(priceFeedVAAs); console.log(Buffer.from(priceFeedVAAs[0], "base64").toString("hex"));
} }
main(); main();

View File

@ -73,6 +73,8 @@ async function update_price_feed_using_accumulator_message(
console.log("PYTH_STATE: ", PYTH_STATE); console.log("PYTH_STATE: ", PYTH_STATE);
console.log("WORM_PACKAGE: ", WORM_PACKAGE); console.log("WORM_PACKAGE: ", WORM_PACKAGE);
console.log("WORM_STATE: ", WORM_STATE); console.log("WORM_STATE: ", WORM_STATE);
console.log("accumulator_message: ", accumulator_message);
console.log("price_info_object_id: ", price_info_object_id);
console.log( console.log(
"vaa parsed from accumulator: ", "vaa parsed from accumulator: ",

View File

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"types": ["mocha", "chai"], "types": ["mocha", "chai", "node"],
"typeRoots": ["./node_modules/@types"], "typeRoots": ["./node_modules/@types"],
"lib": ["es2020", "DOM"], "lib": ["es2020", "DOM"],
"module": "commonjs", "module": "commonjs",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
};

1
target_chains/sui/sdk/js/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
lib

View File

@ -0,0 +1,151 @@
# Pyth Sui JS SDK
[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/).
## Installation
### npm
```
$ npm install --save @pythnetwork/pyth-sui-js
```
### Yarn
```
$ yarn add @pythnetwork/pyth-sui-js
```
## Quickstart
Pyth stores prices off-chain to minimize gas fees, which allows us to offer a wider selection of products and faster update times.
See [On-Demand Updates](https://docs.pyth.network/documentation/pythnet-price-feeds/on-demand) for more information about this approach.
Typically, to use Pyth prices on chain,
they must be fetched from an off-chain price service. The `SuiPriceServiceConnection` class can be used to interact with these services,
providing a way to fetch these prices directly in your code. The following example wraps an existing RPC provider and shows how to obtain
Pyth prices and submit them to the network:
```typescript
const connection = new SuiPriceServiceConnection(
"https://hermes-beta.pyth.network"
); // See Price Service endpoints section below for other endpoints
const priceIds = [
// You can find the ids of prices at https://pyth.network/developers/price-feed-ids#sui-testnet
"0xf9c0172ba10dfa4d19088d94f5bf61d3b54d5bd7483a322a982e1373ee8ea31b", // BTC/USD price id in testnet
"0xca80ba6dc32e08d06f1aa886011eed1d77c77be9eb761cc10d72b7d0a2fd57a6", // ETH/USD price id in testnet
];
// In order to use Pyth prices in your protocol you need to submit the price update data to Pyth contract in your target
// chain. `getPriceUpdateData` creates the update data which can be submitted to your contract.
const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIds);
```
## On-chain prices
### **_Important Note for Integrators_**
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).
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).
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.
You can use `SuiPythClient` to build such transactions.
### Example
```ts
import { SuiPythClient } from "@pythnetwork/pyth-sui-js";
import { TransactionBlock } from "@mysten/sui.js";
const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIds); // see quickstart section
// It is either injected from browser or instantiated in backend via some private key
const wallet: SignerWithProvider = getWallet();
// Get the state ids of the Pyth and Wormhole contracts from
// https://docs.pyth.network/documentation/pythnet-price-feeds/sui
const wormholeStateId = " 0xFILL_ME";
const pythStateId = "0xFILL_ME";
const client = new SuiPythClient(wallet.provider, pythStateId, wormholeStateId);
const tx = new TransactionBlock();
const priceInfoObjectIds = await client.updatePriceFeeds(tx, priceFeedUpdateData, priceIds);
tx.moveCall({
target: `YOUR_PACKAGE::YOUR_MODULE::use_pyth_for_defi`,
arguments: [
..., // other arguments needed for your contract
tx.object(pythStateId),
tx.object(priceInfoObjectIds[0]),
],
});
const txBlock = {
transactionBlock: tx,
options: {
showEffects: true,
showEvents: true,
},
};
const result = await wallet.signAndExecuteTransactionBlock(txBlock);
```
Now in your contract you can consume the price by calling `pyth::get_price` or other utility functions on the `PriceInfoObject`.
### CLI Example
[This example](./src/examples/SuiRelay.ts) shows how to update prices on an Sui network. It does the following:
1. Fetches update data from the Price Service for the given price feeds.
2. Calls the Pyth Sui contract with the update data.
You can run this example with `npm run example-relay`. A full command that updates prices on Sui testnet looks like:
```bash
export SUI_KEY=YOUR_PRIV_KEY;
npm run example-relay -- --feed-id "5a035d5440f5c163069af66062bac6c79377bf88396fa27e6067bfca8096d280" \
--price-service "https://hermes-beta.pyth.network" \
--full-node "https://fullnode.testnet.sui.io:443" \
--pyth-state-id "0xd3e79c2c083b934e78b3bd58a490ec6b092561954da6e7322e1e2b3c8abfddc0" \
--wormhole-state-id "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790"
```
## Off-chain prices
Many applications additionally need to display Pyth prices off-chain, for example, in their frontend application.
The `SuiPriceServiceConnection` provides two different ways to fetch the current Pyth price.
The code blocks below assume that the `connection` and `priceIds` objects have been initialized as shown above.
The first method is a single-shot query:
```typescript
// `getLatestPriceFeeds` returns a `PriceFeed` for each price id. It contains all information about a price and has
// utility functions to get the current and exponentially-weighted moving average price, and other functionality.
const priceFeeds = await connection.getLatestPriceFeeds(priceIds);
// Get the price if it is not older than 60 seconds from the current time.
console.log(priceFeeds[0].getPriceNoOlderThan(60)); // Price { conf: '1234', expo: -8, price: '12345678' }
// Get the exponentially-weighted moving average price if it is not older than 60 seconds from the current time.
console.log(priceFeeds[1].getEmaPriceNoOlderThan(60));
```
The object also supports a streaming websocket connection that allows you to subscribe to every new price update for a given feed.
This method is useful if you want to show continuously updating real-time prices in your frontend:
```typescript
// Subscribe to the price feeds given by `priceId`. The callback will be invoked every time the requested feed
// gets a price update.
connection.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
console.log(
`Received update for ${priceFeed.id}: ${priceFeed.getPriceNoOlderThan(60)}`
);
});
// When using the subscription, make sure to close the websocket upon termination to finish the process gracefully.
setTimeout(() => {
connection.closeWebSocket();
}, 60000);
```
## [Price Service endpoints](https://docs.pyth.network/documentation/pythnet-price-feeds/price-service#public-endpoints)

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

View File

@ -0,0 +1,60 @@
{
"name": "@pythnetwork/pyth-sui-js",
"version": "1.0.0",
"description": "Pyth Network Sui Utilities",
"homepage": "https://pyth.network",
"author": {
"name": "Pyth Data Association"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib/**/*"
],
"repository": {
"type": "git",
"url": "https://github.com/pyth-network/pyth-crosschain",
"directory": "target_chains/sui/sdk/js"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "jest --passWithNoTests",
"build": "tsc",
"example-relay": "npm run build && node lib/examples/SuiRelay.js",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "eslint src/",
"prepublishOnly": "npm run build && npm test && npm run lint",
"preversion": "npm run lint",
"version": "npm run format && git add -A src"
},
"keywords": [
"pyth",
"oracle",
"sui"
],
"license": "Apache-2.0",
"devDependencies": {
"@truffle/hdwallet-provider": "^2.1.5",
"@types/ethereum-protocol": "^1.0.2",
"@types/jest": "^29.4.0",
"@types/node": "^18.11.18",
"@types/web3-provider-engine": "^14.0.1",
"@types/yargs": "^17.0.20",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"eslint": "^8.14.0",
"jest": "^29.4.1",
"prettier": "^2.6.2",
"ts-jest": "^29.0.5",
"typescript": "^4.6.3",
"web3": "^1.8.2",
"yargs": "^17.0.20"
},
"dependencies": {
"@pythnetwork/price-service-client": "*",
"@mysten/sui.js": "^0.32.2",
"buffer": "^6.0.3"
}
}

View File

@ -0,0 +1,21 @@
import {
PriceServiceConnection,
HexString,
} from "@pythnetwork/price-service-client";
import { Buffer } from "buffer";
export class SuiPriceServiceConnection extends PriceServiceConnection {
/**
* Gets price update data (either batch price attestation VAAs or accumulator messages, depending on the chosen endpoint), which then
* can be submitted to the Pyth contract to update the prices. This will throw an axios error if there is a network problem or
* the price service returns a non-ok response (e.g: Invalid price ids)
*
* @param priceIds Array of hex-encoded price ids.
* @returns Array of buffers containing the price update data.
*/
async getPriceFeedsUpdateData(priceIds: HexString[]): Promise<Buffer[]> {
// Fetch the latest price feed update VAAs from the price service
const latestVaas = await this.getLatestVaas(priceIds);
return latestVaas.map((vaa) => Buffer.from(vaa, "base64"));
}
}

View File

@ -0,0 +1,313 @@
import {
JsonRpcProvider,
ObjectId,
SUI_CLOCK_OBJECT_ID,
TransactionBlock,
} from "@mysten/sui.js";
import { HexString } from "@pythnetwork/price-service-client";
export class SuiPythClient {
private pythPackageId: ObjectId | undefined;
private wormholePackageId: ObjectId | undefined;
private priceTableId: ObjectId | undefined;
private priceFeedObjectIdCache: Map<HexString, ObjectId> = new Map();
private baseUpdateFee: number | undefined;
constructor(
public provider: JsonRpcProvider,
public pythStateId: ObjectId,
public wormholeStateId: ObjectId
) {
this.pythPackageId = undefined;
this.wormholePackageId = undefined;
}
async getBaseUpdateFee(): Promise<number> {
if (this.baseUpdateFee === undefined) {
const result = await this.provider.getObject({
id: this.pythStateId,
options: { showContent: true },
});
if (
!result.data ||
!result.data.content ||
result.data.content.dataType !== "moveObject"
)
throw new Error("Unable to fetch pyth state object");
this.baseUpdateFee = result.data.content.fields.base_update_fee as number;
}
return this.baseUpdateFee;
}
/**
* getPackageId returns the latest package id that the object belongs to. Use this to
* fetch the latest package id for a given object id and handle package upgrades automatically.
* @param objectId
* @returns package id
*/
async getPackageId(objectId: ObjectId): Promise<ObjectId> {
const state = await this.provider
.getObject({
id: objectId,
options: {
showContent: true,
},
})
.then((result) => {
if (result.data?.content?.dataType == "moveObject") {
return result.data.content.fields;
}
throw new Error("not move object");
});
if ("upgrade_cap" in state) {
return state.upgrade_cap.fields.package;
}
throw new Error("upgrade_cap not found");
}
/**
* Adds the commands for calling wormhole and verifying the vaas and returns the verified vaas.
* @param vaas array of vaas to verify
* @param tx transaction block to add commands to
*/
async verifyVaas(vaas: Buffer[], tx: TransactionBlock) {
const wormholePackageId = await this.getWormholePackageId();
const verifiedVaas = [];
for (const vaa of vaas) {
const [verifiedVaa] = tx.moveCall({
target: `${wormholePackageId}::vaa::parse_and_verify`,
arguments: [
tx.object(this.wormholeStateId),
tx.pure(Array.from(vaa)),
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
verifiedVaas.push(verifiedVaa);
}
return verifiedVaas;
}
/**
* Adds the necessary commands for updating the pyth price feeds to the transaction block.
* @param tx transaction block to add commands to
* @param updates array of price feed updates received from the price service
* @param feedIds array of feed ids to update (in hex format)
*/
async updatePriceFeeds(
tx: TransactionBlock,
updates: Buffer[],
feedIds: HexString[]
): Promise<ObjectId[]> {
const wormholePackageId = await this.getWormholePackageId();
const packageId = await this.getPythPackageId();
let priceUpdatesHotPotato;
if (updates.every((update) => this.isAccumulatorMsg(update))) {
if (updates.length > 1) {
throw new Error(
"SDK does not support sending multiple accumulator messages in a single transaction"
);
}
const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]);
const verifiedVaas = await this.verifyVaas([vaa], tx);
[priceUpdatesHotPotato] = tx.moveCall({
target: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`,
arguments: [
tx.object(this.pythStateId),
tx.pure(Array.from(updates[0])),
verifiedVaas[0],
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
} else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
const verifiedVaas = await this.verifyVaas(updates, tx);
[priceUpdatesHotPotato] = tx.moveCall({
target: `${packageId}::pyth::create_price_infos_hot_potato`,
arguments: [
tx.object(this.pythStateId),
tx.makeMoveVec({
type: `${wormholePackageId}::vaa::VAA`,
objects: verifiedVaas,
}),
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
} else {
throw new Error("Can't mix accumulator and non-accumulator messages");
}
const priceInfoObjects: ObjectId[] = [];
for (const feedId of feedIds) {
const priceInfoObjectId = await this.getPriceFeedObjectId(feedId);
if (!priceInfoObjectId) {
throw new Error(
`Price feed ${feedId} not found, please create it first`
);
}
priceInfoObjects.push(priceInfoObjectId);
const coin = tx.splitCoins(tx.gas, [
tx.pure(await this.getBaseUpdateFee()),
]);
[priceUpdatesHotPotato] = tx.moveCall({
target: `${packageId}::pyth::update_single_price_feed`,
arguments: [
tx.object(this.pythStateId),
priceUpdatesHotPotato,
tx.object(priceInfoObjectId),
coin,
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
}
tx.moveCall({
target: `${packageId}::hot_potato_vector::destroy`,
arguments: [priceUpdatesHotPotato],
typeArguments: [`${packageId}::price_info::PriceInfo`],
});
return priceInfoObjects;
}
async createPriceFeed(tx: TransactionBlock, updates: Buffer[]) {
const wormholePackageId = await this.getWormholePackageId();
const packageId = await this.getPythPackageId();
if (updates.every((update) => this.isAccumulatorMsg(update))) {
if (updates.length > 1) {
throw new Error(
"SDK does not support sending multiple accumulator messages in a single transaction"
);
}
const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]);
const verifiedVaas = await this.verifyVaas([vaa], tx);
tx.moveCall({
target: `${packageId}::pyth::create_price_feeds_using_accumulator`,
arguments: [
tx.object(this.pythStateId),
tx.pure(Array.from(updates[0])),
verifiedVaas[0],
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
} else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
const verifiedVaas = await this.verifyVaas(updates, tx);
tx.moveCall({
target: `${packageId}::pyth::create_price_feeds`,
arguments: [
tx.object(this.pythStateId),
tx.makeMoveVec({
type: `${wormholePackageId}::vaa::VAA`,
objects: verifiedVaas,
}),
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
} else {
throw new Error("Can't mix accumulator and non-accumulator messages");
}
}
/**
* Get the packageId for the wormhole package if not already cached
*/
async getWormholePackageId() {
if (!this.wormholePackageId) {
this.wormholePackageId = await this.getPackageId(this.wormholeStateId);
}
return this.wormholePackageId;
}
/**
* Get the packageId for the pyth package if not already cached
*/
async getPythPackageId() {
if (!this.pythPackageId) {
this.pythPackageId = await this.getPackageId(this.pythStateId);
}
return this.pythPackageId;
}
/**
* Get the priceFeedObjectId for a given feedId if not already cached
* @param feedId
*/
async getPriceFeedObjectId(feedId: HexString): Promise<ObjectId | undefined> {
const normalizedFeedId = feedId.replace("0x", "");
if (!this.priceFeedObjectIdCache.has(normalizedFeedId)) {
const tableId = await this.getPriceTableId();
const result = await this.provider.getDynamicFieldObject({
parentId: tableId,
name: {
type: `${await this.getPythPackageId()}::price_identifier::PriceIdentifier`,
value: {
bytes: Array.from(Buffer.from(normalizedFeedId, "hex")),
},
},
});
if (!result.data || !result.data.content) {
return undefined;
}
if (result.data.content.dataType !== "moveObject") {
throw new Error("Price feed type mismatch");
}
this.priceFeedObjectIdCache.set(
normalizedFeedId,
result.data.content.fields.value
);
}
return this.priceFeedObjectIdCache.get(normalizedFeedId);
}
/**
* Fetches the price table object id for the current state id if not cached
* @returns price table object id
*/
async getPriceTableId(): Promise<ObjectId> {
if (this.priceTableId === undefined) {
const result = await this.provider.getDynamicFieldObject({
parentId: this.pythStateId,
name: {
type: "vector<u8>",
value: "price_info",
},
});
if (!result.data) {
throw new Error(
"Price Table not found, contract may not be initialized"
);
}
this.priceTableId = result.data.objectId;
}
return this.priceTableId;
}
/**
* Checks if a message is an accumulator message or not
* @param msg - update message from price service
*/
isAccumulatorMsg(msg: Buffer) {
const ACCUMULATOR_MAGIC = "504e4155";
return msg.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC;
}
/**
* Obtains the vaa bytes embedded in an accumulator message.
* @param accumulatorMessage - the accumulator price update message
* @returns vaa bytes as a uint8 array
*/
extractVaaBytesFromAccumulatorMessage(accumulatorMessage: Buffer): Buffer {
if (!this.isAccumulatorMsg(accumulatorMessage)) {
throw new Error("Not an accumulator message");
}
// the first 6 bytes in the accumulator message encode the header, major, and minor bytes
// we ignore them, since we are only interested in the VAA bytes
const trailingPayloadSize = accumulatorMessage.readUint8(6);
const vaaSizeOffset =
7 + // header bytes (header(4) + major(1) + minor(1) + trailing payload size(1))
trailingPayloadSize + // trailing payload (variable number of bytes)
1; // proof_type (1 byte)
const vaaSize = accumulatorMessage.readUint16BE(vaaSizeOffset);
const vaaOffset = vaaSizeOffset + 2;
return accumulatorMessage.subarray(vaaOffset, vaaOffset + vaaSize);
}
}

View File

@ -0,0 +1,83 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
Connection,
Ed25519Keypair,
JsonRpcProvider,
RawSigner,
TransactionBlock,
} from "@mysten/sui.js";
import { SuiPythClient } from "../client";
import { SuiPriceServiceConnection } from "../index";
const argvPromise = yargs(hideBin(process.argv))
.option("feed-id", {
description:
"Price feed ids to update without the leading 0x (e.g f9c0172ba10dfa4d19088d94f5bf61d3b54d5bd7483a322a982e1373ee8ea31b). Can be provided multiple times for multiple feed updates",
type: "array",
demandOption: true,
})
.option("price-service", {
description:
"Endpoint URL for the price service. e.g: https://xc-mainnet.pyth.network",
type: "string",
demandOption: true,
})
.option("full-node", {
description:
"URL of the full Sui node RPC endpoint. e.g: https://fullnode.testnet.sui.io:443",
type: "string",
demandOption: true,
})
.option("pyth-state-id", {
description: "Pyth state object id.",
type: "string",
demandOption: true,
})
.option("wormhole-state-id", {
description: "Wormhole state object id.",
type: "string",
demandOption: true,
}).argv;
export function getProvider(url: string) {
return new JsonRpcProvider(new Connection({ fullnode: url }));
}
async function run() {
if (process.env.SUI_KEY === undefined) {
throw new Error(`SUI_KEY environment variable should be set.`);
}
const argv = await argvPromise;
// Fetch the latest price feed update data from the Price Service
const connection = new SuiPriceServiceConnection(argv["price-service"]);
const feeds = argv["feed-id"] as string[];
const priceFeedUpdateData = await connection.getPriceFeedsUpdateData(feeds);
const provider = getProvider(argv["full-node"]);
const wormholeStateId = argv["wormhole-state-id"];
const pythStateId = argv["pyth-state-id"];
const client = new SuiPythClient(provider, pythStateId, wormholeStateId);
const tx = new TransactionBlock();
await client.updatePriceFeeds(tx, priceFeedUpdateData, feeds);
const wallet = new RawSigner(
Ed25519Keypair.fromSecretKey(Buffer.from(process.env.SUI_KEY, "hex")),
provider
);
const txBlock = {
transactionBlock: tx,
options: {
showEffects: true,
showEvents: true,
},
};
const result = await wallet.signAndExecuteTransactionBlock(txBlock);
console.dir(result, { depth: null });
}
run();

View File

@ -0,0 +1,11 @@
export { SuiPriceServiceConnection } from "./SuiPriceServiceConnection";
export { SuiPythClient } from "./client";
export {
DurationInMs,
HexString,
Price,
PriceFeed,
PriceServiceConnectionConfig,
UnixTimestamp,
} from "@pythnetwork/price-service-client";

View File

@ -0,0 +1,14 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"declaration": true,
"outDir": "./lib",
"rootDir": "src/",
"strict": true,
"esModuleInterop": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}