Ver código fonte

Merge remote-tracking branch 'origin/dev.v2' into drozdziak1/p2w-terra-relay-iface-impl
commit-id:324eae28

Stan Drozd 3 anos atrás
pai
commit
c8e87063c9

+ 1 - 1
Dockerfile.client

@@ -3,7 +3,7 @@ FROM docker.io/library/rust:1.49@sha256:a50165ea96983c21832578afb1c8c028674c965b
 
 RUN apt-get update && apt-get install -yq libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang ncat
 RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs
-RUN curl -sSfL https://release.solana.com/v1.8.1/install | sh
+RUN curl -sSfL https://release.solana.com/v1.9.4/install | sh
 
 RUN rustup default nightly-2022-01-02
 RUN rustup component add rustfmt

+ 31 - 62
ethereum/contracts/pyth/Pyth.sol

@@ -13,21 +13,46 @@ import "./PythGovernance.sol";
 contract Pyth is PythGovernance {
     using BytesLib for bytes;
 
-    function attestPrice(bytes memory encodedVm) public returns (PythStructs.PriceAttestation memory pa) {
+    function attestPriceBatch(bytes memory encodedVm) public returns (PythStructs.BatchPriceAttestation memory bpa) {
         (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
 
         require(valid, reason);
         require(verifyPythVM(vm), "invalid emitter");
 
-        PythStructs.PriceAttestation memory price = parsePriceAttestation(vm.payload);
+        PythStructs.BatchPriceAttestation memory batch = parseBatchPriceAttestation(vm.payload);
 
-        PythStructs.PriceAttestation memory latestPrice = latestAttestation(price.productId, price.priceType);
+        for (uint i = 0; i < batch.attestations.length; i++) {
+            PythStructs.PriceAttestation memory attestation = batch.attestations[i];
 
-        if(price.timestamp > latestPrice.timestamp) {
-            setLatestAttestation(price.productId, price.priceType, price);
+            PythStructs.PriceInfo memory latestPrice = latestPriceInfo(attestation.priceId);
+
+            if(attestation.timestamp > latestPrice.attestation_time) {
+                setLatestPriceInfo(attestation.priceId, newPriceInfo(attestation));
+            }
         }
 
-        return price;
+        return batch;
+    }
+
+    
+    function newPriceInfo(PythStructs.PriceAttestation memory pa) private view returns (PythStructs.PriceInfo memory info) {
+        info.attestation_time = pa.timestamp;
+        info.arrival_time = block.timestamp;
+        info.arrival_block = block.number;
+        
+        info.price.id = pa.priceId;
+        info.price.price = pa.price;
+        info.price.conf = pa.confidenceInterval;
+        info.price.status = PythSDK.PriceStatus(pa.status);
+        info.price.expo = pa.exponent;
+        info.price.emaPrice = pa.emaPrice.value;
+        info.price.emaConf = uint64(pa.emaConf.value);
+        info.price.productId = pa.productId;
+
+        // These aren't sent in the wire format yet
+        info.price.numPublishers = 0;
+        info.price.maxNumPublishers = 0;
+        return info;
     }
 
     function verifyPythVM(IWormhole.VM memory vm) public view returns (bool valid) {
@@ -126,60 +151,4 @@ contract Pyth is PythGovernance {
             index += 8;
         }
     }
-
-    function parsePriceAttestation(bytes memory encodedPriceAttestation) public pure returns (PythStructs.PriceAttestation memory pa) {
-        uint index = 0;
-
-        pa.header.magic = encodedPriceAttestation.toUint32(index);
-        index += 4;
-        require(pa.header.magic == 0x50325748, "invalid protocol");
-
-        pa.header.version = encodedPriceAttestation.toUint16(index);
-        index += 2;
-        require(pa.header.version == 1, "invalid protocol");
-
-        pa.header.payloadId = encodedPriceAttestation.toUint8(index);
-        index += 1;
-        require(pa.header.payloadId == 1, "invalid PriceAttestation");
-
-        pa.productId = encodedPriceAttestation.toBytes32(index);
-        index += 32;
-        pa.priceId = encodedPriceAttestation.toBytes32(index);
-        index += 32;
-
-        pa.priceType = encodedPriceAttestation.toUint8(index);
-        index += 1;
-
-        pa.price = int64(encodedPriceAttestation.toUint64(index));
-        index += 8;
-        pa.exponent = int32(encodedPriceAttestation.toUint32(index));
-        index += 4;
-
-        pa.emaPrice.value = int64(encodedPriceAttestation.toUint64(index));
-        index += 8;
-        pa.emaPrice.numerator = int64(encodedPriceAttestation.toUint64(index));
-        index += 8;
-        pa.emaPrice.denominator = int64(encodedPriceAttestation.toUint64(index));
-        index += 8;
-
-        pa.emaConf.value = int64(encodedPriceAttestation.toUint64(index));
-        index += 8;
-        pa.emaConf.numerator = int64(encodedPriceAttestation.toUint64(index));
-        index += 8;
-        pa.emaConf.denominator = int64(encodedPriceAttestation.toUint64(index));
-        index += 8;
-
-        pa.confidenceInterval = encodedPriceAttestation.toUint64(index);
-        index += 8;
-
-        pa.status = encodedPriceAttestation.toUint8(index);
-        index += 1;
-        pa.corpAct = encodedPriceAttestation.toUint8(index);
-        index += 1;
-
-        pa.timestamp = encodedPriceAttestation.toUint64(index);
-        index += 8;
-
-        require(encodedPriceAttestation.length == index, "invalid PriceAttestation");
-    }
 }

+ 2 - 2
ethereum/contracts/pyth/PythGetters.sol

@@ -40,7 +40,7 @@ contract PythGetters is PythState {
         return _state.provider.pyth2WormholeEmitter;
     }
 
-    function latestAttestation(bytes32 product, uint8 priceType) public view returns (PythStructs.PriceAttestation memory attestation){
-        return _state.latestAttestations[product][priceType];
+    function latestPriceInfo(bytes32 priceId) public view returns (PythStructs.PriceInfo memory info){
+        return _state.latestPriceInfo[priceId];
     }
 }

+ 46 - 0
ethereum/contracts/pyth/PythSDK.sol

@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../libraries/external/BytesLib.sol";
+
+contract PythSDK {
+    
+    // Price represents a current aggregation price from pyth publisher feeds.
+    struct Price {
+        // The price ID.
+        bytes32 id;
+        // Product account key.
+        bytes32 productId;
+        // The current price.
+        int64 price;
+        // Confidence interval around the price.
+        uint64 conf;
+        // Price exponent.
+        int32 expo;
+        // Status of price.
+        PriceStatus status;
+        // Maximum number of allowed publishers that can contribute to a price.
+        uint32 maxNumPublishers;
+        // Number of publishers that made up current aggregate.
+        uint32 numPublishers;
+        // Exponentially moving average price.
+        int64 emaPrice;
+        // Exponentially moving average confidence interval.
+        uint64 emaConf;
+    }
+
+    /* PriceStatus represents the availability status of a price feed.
+        UNKNOWN: The price feed is not currently updating for an unknown reason.
+        TRADING: The price feed is updating as expected.
+        HALTED: The price feed is not currently updating because trading in the product has been halted.
+        AUCTION: The price feed is not currently updating because an auction is setting the price.
+    */
+    enum PriceStatus {
+        UNKNOWN,
+        TRADING,
+        HALTED,
+        AUCTION
+    }
+
+}

+ 2 - 2
ethereum/contracts/pyth/PythSetters.sol

@@ -38,7 +38,7 @@ contract PythSetters is PythState {
         _state.wormhole = payable(wh);
     }
 
-    function setLatestAttestation(bytes32 product, uint8 priceType, PythStructs.PriceAttestation memory attestation) internal {
-        _state.latestAttestations[product][priceType] = attestation;
+    function setLatestPriceInfo(bytes32 priceId, PythStructs.PriceInfo memory info) internal {
+        _state.latestPriceInfo[priceId] = info;
     }
 }

+ 3 - 3
ethereum/contracts/pyth/PythState.sol

@@ -27,9 +27,9 @@ contract PythStorage {
         // Mapping of initialized implementations
         mapping(address => bool) initializedImplementations;
 
-        // Mapping of cached price attestations
-        // productId => priceType => PriceAttestation
-        mapping(bytes32 => mapping(uint8 => PythStructs.PriceAttestation)) latestAttestations;
+        // Mapping of cached price information
+        // priceId => PriceInfo
+        mapping(bytes32 => PythStructs.PriceInfo) latestPriceInfo;
     }
 }
 

+ 8 - 0
ethereum/contracts/pyth/PythStructs.sol

@@ -4,6 +4,7 @@
 pragma solidity ^0.8.0;
 
 import "../libraries/external/BytesLib.sol";
+import "./PythSDK.sol";
 
 contract PythStructs {
     using BytesLib for bytes;
@@ -56,4 +57,11 @@ contract PythStructs {
 
         address newContract;
     }
+
+    struct PriceInfo {
+        PythSDK.Price price;
+        uint256 attestation_time;
+        uint256 arrival_time;
+        uint256 arrival_block;
+    }
 }

+ 101 - 60
ethereum/test/pyth.js

@@ -2,6 +2,7 @@ const jsonfile = require('jsonfile');
 const elliptic = require('elliptic');
 const BigNumber = require('bignumber.js');
 
+const PythSDK = artifacts.require("PythSDK");
 const Wormhole = artifacts.require("Wormhole");
 const PythDataBridge = artifacts.require("PythDataBridge");
 const PythImplementation = artifacts.require("PythImplementation");
@@ -91,42 +92,9 @@ contract("Pyth", function () {
         assert.ok(isUpgraded);
     })
 
-    let testUpdate = "0x"+
-        "503257480001011515151515151515151515151515151515151515151515151515151515151515DEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDEDE01DEADBEEFDEADBABEFFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE0000000000000065010000000000075BCD15";
-
-    it("should parse price update correctly", async function() {
-        const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
-
-        let parsed = await initialized.methods.parsePriceAttestation(testUpdate).call();
-
-        assert.equal(parsed.header.magic, 1345476424);
-        assert.equal(parsed.header.version, 1);
-        assert.equal(parsed.header.payloadId, 1);
-        assert.equal(parsed.productId, "0x1515151515151515151515151515151515151515151515151515151515151515");
-        assert.equal(parsed.priceId, "0xdededededededededededededededededededededededededededededededede");
-        assert.equal(parsed.priceType, 1);
-        assert.equal(parsed.price, -2401053088876217666);
-        assert.equal(parsed.exponent, -3);
-
-        assert.equal(parsed.emaPrice.value, -42);
-        assert.equal(parsed.emaPrice.numerator, 15);
-        assert.equal(parsed.emaPrice.denominator, 37);
-
-        assert.equal(parsed.emaConf.value, 42);
-        assert.equal(parsed.emaConf.numerator, 1111);
-        assert.equal(parsed.emaConf.denominator, 2222);
-
-        assert.equal(parsed.confidenceInterval, 101);
-
-        assert.equal(parsed.status, 1);
-        assert.equal(parsed.corpAct, 0);
-
-        assert.equal(parsed.timestamp, 123456789);
-    })
+    const rawBatchPriceAttestation = "0x"+"503257480002020004009650325748000201c0e11df4c58a4e53f2bc059ba57a7c8f30ddada70b5bdc3753f90b824b64dd73c1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e01000000000000071dfffffffb00000000000005f70000000132959bbd00000000c8bfed5f00000000000000030000000041c7b65b00000000c8bfed5f0000000000000003010000000000622f65f4503257480002017090c4ecf0309718d04c5a162c08aa4b78f533f688fa2f3ccd7be74c2a253a54fd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620010000000000000440fffffffb00000000000005fb000000015cfe8c9d00000000e3dbaa7f00000000000000020000000041c7c5bb00000000e3dbaa7f0000000000000007010000000000622f65f4503257480002012f064374f55cb2efbbef29329de3b652013a76261876c55a1caf3a489c721ccd8c5dd422900917e8e26316fe598e8f062058d390644e0e36d42c187298420ccd010000000000000609fffffffb00000000000005cd00000001492c19bd00000000dd92071f00000000000000020000000041c7d3fb00000000dd92071f0000000000000001010000000000622f65f45032574800020171ddabd1a2c1fb6d6c4707b245b7c0ab6af0ae7b96b2ff866954a0b71124aee517fbe895e5416ddb4d5af9d83c599ee2c4f94cb25e8597f9e5978bd63a7cdcb70100000000000007bcfffffffb00000000000005e2000000014db2995d00000000dd8f775f00000000000000020000000041c7df9b00000000dd8f775f0000000000000003010000000000622f65f4";
 
     it("should parse batch price attestation correctly", async function() {
-        let rawBatchPriceAttestation = "0x"+"503257480002020004009650325748000201c0e11df4c58a4e53f2bc059ba57a7c8f30ddada70b5bdc3753f90b824b64dd73c1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e01000000000000071dfffffffb00000000000005f70000000132959bbd00000000c8bfed5f00000000000000030000000041c7b65b00000000c8bfed5f0000000000000003010000000000622f65f4503257480002017090c4ecf0309718d04c5a162c08aa4b78f533f688fa2f3ccd7be74c2a253a54fd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620010000000000000440fffffffb00000000000005fb000000015cfe8c9d00000000e3dbaa7f00000000000000020000000041c7c5bb00000000e3dbaa7f0000000000000007010000000000622f65f4503257480002012f064374f55cb2efbbef29329de3b652013a76261876c55a1caf3a489c721ccd8c5dd422900917e8e26316fe598e8f062058d390644e0e36d42c187298420ccd010000000000000609fffffffb00000000000005cd00000001492c19bd00000000dd92071f00000000000000020000000041c7d3fb00000000dd92071f0000000000000001010000000000622f65f45032574800020171ddabd1a2c1fb6d6c4707b245b7c0ab6af0ae7b96b2ff866954a0b71124aee517fbe895e5416ddb4d5af9d83c599ee2c4f94cb25e8597f9e5978bd63a7cdcb70100000000000007bcfffffffb00000000000005e2000000014db2995d00000000dd8f775f00000000000000020000000041c7df9b00000000dd8f775f0000000000000003010000000000622f65f4";
-
         const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
 
         const magic = 1345476424;
@@ -225,8 +193,7 @@ contract("Pyth", function () {
         assert.equal(parsed.attestations[3].timestamp, 1647273460);
     })
 
-    it("should attest price updates over wormhole", async function() {
-        const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
+    async function attest(contract, data) {
         const accounts = await web3.eth.getAccounts();
 
         const vm = await signAndEncodeVM(
@@ -235,7 +202,7 @@ contract("Pyth", function () {
             testPyth2WormholeChainId,
             testPyth2WormholeEmitter,
             0,
-            testUpdate,
+            data,
             [
                 testSigner1PK
             ],
@@ -243,41 +210,115 @@ contract("Pyth", function () {
             0
         );
 
-        let result = await initialized.methods.attestPrice("0x"+vm).send({
+        let result = await contract.methods.attestPriceBatch("0x"+vm).send({
             value : 0,
             from : accounts[0],
             gasLimit : 2000000
         });
-    })
+    }
 
-    it("should cache price updates", async function() {
+    it("should attest price updates over wormhole", async function() {
         const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
 
-        let cached = await initialized.methods.latestAttestation("0x1515151515151515151515151515151515151515151515151515151515151515", 1).call();
-
-        assert.equal(cached.header.magic, 1345476424);
-        assert.equal(cached.header.version, 1);
-        assert.equal(cached.header.payloadId, 1);
-        assert.equal(cached.productId, "0x1515151515151515151515151515151515151515151515151515151515151515");
-        assert.equal(cached.priceId, "0xdededededededededededededededededededededededededededededededede");
-        assert.equal(cached.priceType, 1);
-        assert.equal(cached.price, -2401053088876217666);
-        assert.equal(cached.exponent, -3);
+        await attest(initialized, rawBatchPriceAttestation);
+    })
 
-        assert.equal(cached.emaPrice.value, -42);
-        assert.equal(cached.emaPrice.numerator, 15);
-        assert.equal(cached.emaPrice.denominator, 37);
+    it("should cache price updates", async function() {
+        const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
 
-        assert.equal(cached.emaConf.value, 42);
-        assert.equal(cached.emaConf.numerator, 1111);
-        assert.equal(cached.emaConf.denominator, 2222);
+        await attest(initialized, rawBatchPriceAttestation);
+
+        let first = await initialized.methods.latestPriceInfo("0xc1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e").call();
+        assert.equal(first.price.id, "0xc1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e");
+        assert.equal(first.price.productId, "0xc0e11df4c58a4e53f2bc059ba57a7c8f30ddada70b5bdc3753f90b824b64dd73");
+        assert.equal(first.price.price, 1821);
+        assert.equal(first.price.conf, 3);
+        assert.equal(first.price.expo, -5);
+        assert.equal(first.price.status.toString(), PythSDK.PriceStatus.TRADING.toString());
+        assert.equal(first.price.numPublishers, 0);
+        assert.equal(first.price.maxNumPublishers, 0);
+        assert.equal(first.price.emaPrice, 1527);
+        assert.equal(first.price.emaConf, 3);
+        assert.equal(first.attestation_time, 1647273460);
+
+        let second = await initialized.methods.latestPriceInfo("0xfd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620").call();
+        assert.equal(second.price.id, "0xfd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620");
+        assert.equal(second.price.productId, "0x7090c4ecf0309718d04c5a162c08aa4b78f533f688fa2f3ccd7be74c2a253a54");
+        assert.equal(second.price.price, 1088);
+        assert.equal(second.price.conf, 7);
+        assert.equal(second.price.expo, -5);
+        assert.equal(second.price.status.toString(), PythSDK.PriceStatus.TRADING.toString());
+        assert.equal(second.price.numPublishers, 0);
+        assert.equal(second.price.maxNumPublishers, 0);
+        assert.equal(second.price.emaPrice, 1531);
+        assert.equal(second.price.emaConf, 2);
+        assert.equal(second.attestation_time, 1647273460);
+    })
 
-        assert.equal(cached.confidenceInterval, 101);
+    it("should only cache updates for new prices", async function() {
+        // This test sends two batches of updates, for the same Price IDs. The second batch contains
+        // different price values to the first batch, but only the first and last updates in
+        // the second batch have a newer timestamp than those in the first batch, and so these 
+        // are the only two which should be cached.
 
-        assert.equal(cached.status, 1);
-        assert.equal(cached.corpAct, 0);
+        const initialized = new web3.eth.Contract(P2WImplementationFullABI, PythDataBridge.address);
 
-        assert.equal(cached.timestamp, 123456789);
+        let secondBatchPriceAttestation = "0x"+"503257480002020004009650325748000201c0e11df4c58a4e53f2bc059ba57a7c8f30ddada70b5bdc3753f90b824b64dd73c1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e01000000000000073dfffffffb00000000000005470000000132959bbd00000000c8bfed5f00000000000000030000000041c7b65b00000000c8bfed5f0000000000000003010000000000622f65f5503257480002017090c4ecf0309718d04c5a162c08aa4b78f533f688fa2f3ccd7be74c2a253a54fd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620010000000000000450fffffffb00000000000005fb000000015cfe8c9d00000000e3dbaa7f00000000000000020000000041c7c5bb00000000e3dbaa7f0000000000000007010000000000622f65f4503257480002012f064374f55cb2efbbef29329de3b652013a76261876c55a1caf3a489c721ccd8c5dd422900917e8e26316fe598e8f062058d390644e0e36d42c187298420ccd010000000000000659fffffffb00000000000005cd00000001492c19bd00000000dd92071f00000000000000020000000041c7d3fb00000000dd92071f0000000000000001010000000000622f65f45032574800020181ddabd1a2c1fb6d6c4707b245b7c0ab6af0ae7b96b2ff866954a0b71124aee517fbe895e5416ddb4d5af9d83c599ee2c4f94cb25e8597f9e5978bd63a7cdcb70100000000000007bDfffffffb00000000000005e2000000014db2995d00000000dd8f775f00000000000000020000000041c7df9b00000000dd8f775f0000000000000003010000000000622f65f5";
+
+        let all_price_ids = ["0xc1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e",
+            "0xfd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620",
+            "0x8c5dd422900917e8e26316fe598e8f062058d390644e0e36d42c187298420ccd",
+            "0x17fbe895e5416ddb4d5af9d83c599ee2c4f94cb25e8597f9e5978bd63a7cdcb7"
+        ];
+
+        // Send the first batch
+        await attest(initialized, rawBatchPriceAttestation);
+        let prices_after_first_update = {};
+        for (var i = 0; i < all_price_ids.length; i++) {
+            const price_id = all_price_ids[i];
+            prices_after_first_update[price_id] = await initialized.methods.latestPriceInfo(price_id).call();
+        }
+
+        // Send the second batch
+        await attest(initialized, secondBatchPriceAttestation);
+        let prices_after_second_update = {};
+        for (var i = 0; i < all_price_ids.length; i++) {
+            const price_id = all_price_ids[i];
+            prices_after_second_update[price_id] = await initialized.methods.latestPriceInfo(price_id).call();
+        }
+
+        // Price IDs which have newer timestamps
+        let new_price_updates = [
+            "0xc1902e05cdf03bc089a943d921f87ccd0e3e1b774b5660d037b9f428c0d3305e",
+            "0x17fbe895e5416ddb4d5af9d83c599ee2c4f94cb25e8597f9e5978bd63a7cdcb7"
+        ];
+
+        // Price IDs which have older timestamps
+        let old_price_updates = [
+            "0xfd4caca566fc44a9d6585420959d13897877c606477b3f0e7f247295b7275620",
+            "0x8c5dd422900917e8e26316fe598e8f062058d390644e0e36d42c187298420ccd"];
+
+        // Check that the new price updates have been updated
+        for (var i = 0; i < new_price_updates.length; i++) {
+            const price_id = new_price_updates[i];
+            assert.notEqual(prices_after_first_update[price_id].price.price, prices_after_second_update[price_id].price.price);
+            assert.notEqual(prices_after_first_update[price_id].attestation_time, prices_after_second_update[price_id].attestation_time);
+        }
+
+        // Check that the old price updates have been discarded
+        for (var i = 0; i < old_price_updates.length; i++) {
+            const price_id = old_price_updates[i];
+            assert.equal(prices_after_first_update[price_id].price.price, prices_after_second_update[price_id].price.price);
+            assert.equal(prices_after_first_update[price_id].price.conf, prices_after_second_update[price_id].price.conf);
+            assert.equal(prices_after_first_update[price_id].price.expo, prices_after_second_update[price_id].price.expo);
+            assert.equal(prices_after_first_update[price_id].price.status.toString(), prices_after_second_update[price_id].price.status.toString());
+            assert.equal(prices_after_first_update[price_id].price.numPublishers, prices_after_second_update[price_id].price.numPublishers);
+            assert.equal(prices_after_first_update[price_id].price.maxNumPublishers, prices_after_second_update[price_id].price.maxNumPublishers);
+            assert.equal(prices_after_first_update[price_id].price.emaPrice, prices_after_second_update[price_id].price.emaPrice);
+            assert.equal(prices_after_first_update[price_id].price.emaConf, prices_after_second_update[price_id].price.emaConf);
+            assert.equal(prices_after_first_update[price_id].attestation_time, prices_after_second_update[price_id].attestation_time);
+            assert.equal(prices_after_first_update[price_id].arrival_time, prices_after_second_update[price_id].arrival_time);
+        }
     })
 });
 

+ 1 - 0
terra/.gitignore

@@ -0,0 +1 @@
+artifacts/

+ 6 - 5
terra/Cargo.lock

@@ -1320,7 +1320,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "p2w-sdk"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "pyth-sdk-solana",
  "serde",
@@ -1433,6 +1433,7 @@ dependencies = [
  "k256",
  "lazy_static",
  "p2w-sdk",
+ "pyth-sdk",
  "schemars",
  "serde",
  "serde_derive",
@@ -1446,9 +1447,9 @@ dependencies = [
 
 [[package]]
 name = "pyth-sdk"
-version = "0.1.0"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "446ff07d7ef3bd98214f9b4fe6a611a69e36b5aad74b18cdbad5150193c1f204"
+checksum = "c610102a39fc4bae29a3b5a628ee134d25afb3dca3937692f5e634f1287fe0b4"
 dependencies = [
  "borsh",
  "borsh-derive",
@@ -1458,9 +1459,9 @@ dependencies = [
 
 [[package]]
 name = "pyth-sdk-solana"
-version = "0.1.0"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27a648739aa69cab94edd900a0d7ca37d8a789e9c88741b23deec11fab418d16"
+checksum = "b1fdc94592a28fa829b0d6fa619392b1a1744048e5b78a74a4ba93cf541eddae"
 dependencies = [
  "borsh",
  "borsh-derive",

+ 88 - 0
terra/README.pyth.md

@@ -0,0 +1,88 @@
+# Intro
+
+Deploying a contract in terra consists of two steps:
+1. Uploading the code. This step will give you a code id.
+2. Optionally create a new contract or migrate an existing one:
+    1. Creating a new contract which has an address with a code id as its program.
+    2. Migrating an existing contract code id to the new code id.
+
+This script can do both steps at the same time. Read below for the details.
+
+# Uploading the code
+
+First build the contracts:
+
+``` sh
+bash build.sh
+```
+
+This command will builds and saves all the contracts in the `artifact` directory.
+
+Then, for example, to deploy `pyth_bridge.wasm`, run in the `tools` directory:
+
+``` sh
+npm ci # Do it only once to install required packages
+npm run deploy-pyth -- --network testnet --artifact ../artifacts/pyth_bridge.wasm --mnemonic "..."
+```
+
+which will print something along the lines of:
+
+``` sh
+Storing WASM: ../artifacts/pyth_bridge.wasm (367689 bytes)
+Deploy fee:  88446uluna
+Code ID:  2435
+```
+
+If you do not pass any additional arguments to the script it will only upload the code and returns the code id. If you want to create a 
+new contract or upgrade an existing contract you should pass more arguments that are described below.
+
+# Instantiating new contract
+If you want instantiate a new contract after your deployment pass `--instantiate` argument to the above command.
+It will upload the code and with the resulting code id instantiates a new pyth contract:
+
+``` sh
+npm run deploy-pyth -- --network testnet --artifact ../artifacts/pyth_bridge.wasm --mnemonic "..." --instantiate
+```
+
+If successful, the output should look like:
+```
+Storing WASM: ../artifacts/pyth_bridge.wasm (183749 bytes)
+Deploy fee:  44682uluna
+Code ID:  53199
+Instantiating a contract
+Sleeping for 10 seconds for store transaction to finalize.
+Instantiated Pyth Bridge at terra123456789yelw23uh22nadqlyjvtl7s5527er97 (0x0000000000000000000000001234567896267ee5479752a7d683e49317ff4294)
+Deployed pyth contract at terra123456789yelw23uh22nadqlyjvtl7s5527er97
+```
+
+# Migrating existing contract
+If you want to upgrade an existing contract pass `--migrate --contract terra123456xyzqwe..` arguments to the above command.
+It will upload the code and with the resulting code id migrates the existing contract to the new one:
+
+``` sh
+npm run deploy-pyth -- --network testnet --artifact ../artifacts/pyth_bridge.wasm --mnemonic "..." --migrate --contract "terra123..."
+```
+
+If successful, the output should look like:
+```
+Storing WASM: ../artifacts/pyth_bridge.wasm (183749 bytes)
+Deploy fee:  44682uluna
+Code ID:  53227
+Sleeping for 10 seconds for store transaction to finalize.
+Migrating contract terra1rhjej5gkyelw23uh22nadqlyjvtl7s5527er97 to 53227
+Contract terra1rhjej5gkyelw23uh22nadqlyjvtl7s5527er97 code_id successfully updated to 53227
+```
+
+# Notes
+
+You might encounter gateway timeout or account sequence mismatch in errors. In is good to double check with terra finder as sometimes
+transactions succeed despite being timed out.
+
+If that happens in the middle of an instantiation or migration. You can avoid re-uploading the code and use the resulting Code Id 
+by passing `--code-id <codeId>` instead of `--artifact` and it will only do the instantiation/migration part.
+
+An example is:
+
+``` sh
+npm run deploy-pyth -- --network testnet --code-id 50123 --mnemonic "..." --migrate --contract "terra123..."
+```

+ 2 - 1
terra/build.sh

@@ -1,6 +1,7 @@
 #!/usr/bin/env bash
 
 docker run --rm -v "$(pwd)":/code \
+  -v $(cd ../third_party; pwd):/third_party \
   --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
   --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
-  cosmwasm/workspace-optimizer:0.12.1
+  cosmwasm/workspace-optimizer:0.12.5

+ 1 - 0
terra/contracts/pyth-bridge/Cargo.toml

@@ -33,6 +33,7 @@ lazy_static = "1.4.0"
 bigint = "4"
 p2w-sdk = { path = "../../../third_party/pyth/p2w-sdk/rust" }
 solana-program = "=1.8.16"
+pyth-sdk = { version = "0.2.0" }
 
 [dev-dependencies]
 cosmwasm-vm = { version = "0.16.0", default-features = false }

+ 100 - 35
terra/contracts/pyth-bridge/src/contract.rs

@@ -8,9 +8,14 @@ use cosmwasm_std::{
     MessageInfo,
     QueryRequest,
     Response,
-    StdError,
     StdResult,
     WasmQuery,
+    Timestamp,
+};
+
+use pyth_sdk::{
+    PriceFeed,
+    PriceStatus,
 };
 
 use crate::{
@@ -19,21 +24,21 @@ use crate::{
         InstantiateMsg,
         MigrateMsg,
         QueryMsg,
+        PriceFeedResponse,
     },
     state::{
         config,
         config_read,
         price_info,
         price_info_read,
-        sequence,
-        sequence_read,
         ConfigInfo,
+        PriceInfo,
+        VALID_TIME_PERIOD,
     },
 };
 
 use p2w_sdk::{
     BatchPriceAttestation,
-    PriceAttestation,
 };
 
 use wormhole::{
@@ -57,14 +62,13 @@ pub fn instantiate(
     _info: MessageInfo,
     msg: InstantiateMsg,
 ) -> StdResult<Response> {
-    // Save general wormhole info
+    // Save general wormhole and pyth info
     let state = ConfigInfo {
         wormhole_contract: msg.wormhole_contract,
         pyth_emitter: msg.pyth_emitter.as_slice().to_vec(),
         pyth_emitter_chain: msg.pyth_emitter_chain,
     };
     config(deps.storage).save(&state)?;
-    sequence(deps.storage).save(&0)?;
 
     Ok(Response::default())
 }
@@ -97,57 +101,118 @@ fn submit_vaa(
     let state = config_read(deps.storage).load()?;
 
     let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
-    let data = vaa.payload;
-
-    // IMPORTANT: VAA replay-protection is not implemented in this code-path
-    // Sequences are used to prevent replay or price rollbacks
-
-    let message = BatchPriceAttestation::deserialize(&data[..])
-        .map_err(|_| ContractError::InvalidVAA.std())?;
+ 
+    // This checks the emitter to be the pyth emitter in wormhole and it comes from emitter chain (Solana)
     if vaa.emitter_address != state.pyth_emitter || vaa.emitter_chain != state.pyth_emitter_chain {
         return ContractError::InvalidVAA.std_err();
     }
 
-    // Check sequence
-    let last_sequence = sequence_read(deps.storage).load()?;
-    if vaa.sequence <= last_sequence && last_sequence != 0 {
-        return Err(StdError::generic_err(
-            "price sequences need to be monotonically increasing",
-        ));
-    }
-    sequence(deps.storage).save(&vaa.sequence)?;
+    let data = vaa.payload;
+
+    let message = BatchPriceAttestation::deserialize(&data[..])
+        .map_err(|_| ContractError::InvalidVAA.std())?;
+    
+    let mut new_attestations_cnt: u8 = 0;
 
-    // Update price
+    // Update prices
     for price_attestation in message.price_attestations.iter() {
-        price_info(deps.storage).save(
-            &price_attestation.price_id.to_bytes()[..],
-            &price_attestation.serialize(),
-        )?;
+        let price_feed = PriceFeed::new(
+                price_attestation.price_id.to_bytes(),
+                price_attestation.status,
+                price_attestation.expo,
+                0, // max_num_publishers data is currently unavailable
+                0, // num_publishers data is currently unavailable
+                price_attestation.product_id.to_bytes(),
+                price_attestation.price,
+            price_attestation.confidence_interval,
+            price_attestation.ema_price.val,
+            price_attestation.ema_conf.val as u64,
+        );
+
+        let attestation_time = Timestamp::from_seconds(price_attestation.timestamp as u64);
+
+        price_info(deps.storage).update(
+            price_feed.id.as_ref(),
+        |maybe_price_info| -> StdResult<PriceInfo> {
+            match maybe_price_info {
+                Some(price_info) => {
+                    // This check ensures that a price won't be updated with the same or older message.
+                    // Attestation_time is guaranteed increasing in solana
+                    if price_info.attestation_time < attestation_time {
+                        new_attestations_cnt += 1;
+                        Ok(PriceInfo {
+                            arrival_time: env.block.time,
+                            arrival_block: env.block.height,
+                            price_feed,
+                            attestation_time
+                        })
+                    } else {
+                        Ok(price_info)
+                    }
+                },
+                None => {
+                    new_attestations_cnt += 1;
+                    Ok(PriceInfo {
+                        arrival_time: env.block.time,
+                        arrival_block: env.block.height,
+                        price_feed,
+                        attestation_time
+                    })
+                }
+            }
+        })?;
     }
 
     Ok(Response::new()
         .add_attribute("action", "price_update")
         .add_attribute(
-            "num_price_feeds",
+            "batch_size",
             format!("{}", message.price_attestations.len()),
-        ))
+        )
+        .add_attribute(
+            "num_updates",
+            format!("{}", new_attestations_cnt),
+        )
+    )
 }
 
 
 #[cfg_attr(not(feature = "library"), entry_point)]
-pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
-        QueryMsg::PriceInfo { price_id } => {
-            to_binary(&query_price_info(deps, price_id.as_slice())?)
+        QueryMsg::PriceFeed { id } => {
+            to_binary(&query_price_info(deps, env, id.as_ref())?)
         }
     }
 }
 
-pub fn query_price_info(deps: Deps, address: &[u8]) -> StdResult<PriceAttestation> {
+pub fn query_price_info(deps: Deps, env: Env, address: &[u8]) -> StdResult<PriceFeedResponse> {
     match price_info_read(deps.storage).load(address) {
-        Ok(data) => PriceAttestation::deserialize(&data[..]).map_err(|_| {
-            StdError::parse_err("PriceAttestation", "failed to decode price attestation")
-        }),
+        Ok(mut terra_price_info) => {
+            // Attestation time is very close to the actual price time (maybe a few seconds older).
+            // Cases that it will cover:
+            // - This will ensure to set status unknown if the price has become very old and hasn't updated yet.
+            // - If a price has arrived very late to terra it will set the status to unknown.
+            // - If a price is coming from future it's tolerated up to VALID_TIME_PERIOD seconds (using abs diff)
+            //   but more than that is set to unknown, the reason is huge clock difference means there exists a 
+            //   problem in a either Terra or Solana blockchain and if it is Solana we don't want to propagate
+            //   Solana internal problems to Terra
+            let time_abs_diff = if env.block.time.seconds() > terra_price_info.attestation_time.seconds() {
+                env.block.time.seconds() - terra_price_info.attestation_time.seconds()
+            } else {
+                terra_price_info.attestation_time.seconds() - env.block.time.seconds()
+            };
+
+            if time_abs_diff > VALID_TIME_PERIOD.as_secs() {
+                terra_price_info.price_feed.status = PriceStatus::Unknown;
+            }
+
+            Ok(
+                PriceFeedResponse {
+                    price_feed: terra_price_info.price_feed,
+                }
+            )
+        },
         Err(_) => ContractError::AssetNotFound.std_err(),
     }
 }

+ 12 - 2
terra/contracts/pyth-bridge/src/msg.rs

@@ -1,4 +1,7 @@
-use cosmwasm_std::Binary;
+use cosmwasm_std::{
+    Binary,
+};
+use pyth_sdk::{PriceFeed, PriceIdentifier};
 use schemars::JsonSchema;
 use serde::{
     Deserialize,
@@ -27,5 +30,12 @@ pub struct MigrateMsg {}
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum QueryMsg {
-    PriceInfo { price_id: Binary },
+    PriceFeed { id: PriceIdentifier },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct PriceFeedResponse {
+    /// Pyth Price Feed
+    pub price_feed:        PriceFeed,
 }

+ 23 - 12
terra/contracts/pyth-bridge/src/state.rs

@@ -1,3 +1,6 @@
+use std::time::Duration;
+
+use pyth_sdk::PriceFeed;
 use schemars::JsonSchema;
 use serde::{
     Deserialize,
@@ -7,7 +10,9 @@ use serde::{
 use cosmwasm_std::{
     StdResult,
     Storage,
+    Timestamp,
 };
+
 use cosmwasm_storage::{
     bucket,
     bucket_read,
@@ -24,8 +29,12 @@ use wormhole::byte_utils::ByteUtils;
 type HumanAddr = String;
 
 pub static CONFIG_KEY: &[u8] = b"config";
-pub static PRICE_INFO_KEY: &[u8] = b"price_info";
-pub static SEQUENCE_KEY: &[u8] = b"sequence";
+pub static PRICE_INFO_KEY: &[u8] = b"price_info_v2";
+
+/// Maximum acceptable time period before price is considered to be stale.
+/// 
+/// This value considers attestation delay which currently might up to a minute.
+pub const VALID_TIME_PERIOD: Duration = Duration::from_secs(3*60);
 
 // Guardian set information
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -35,6 +44,16 @@ pub struct ConfigInfo {
     pub pyth_emitter_chain: u16,
 }
 
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct PriceInfo {
+    pub arrival_time:         Timestamp,
+    pub arrival_block:        u64,
+    pub attestation_time:     Timestamp,
+    pub price_feed:           PriceFeed,
+}
+
 pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
     singleton(storage, CONFIG_KEY)
 }
@@ -43,19 +62,11 @@ pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
     singleton_read(storage, CONFIG_KEY)
 }
 
-pub fn sequence(storage: &mut dyn Storage) -> Singleton<u64> {
-    singleton(storage, SEQUENCE_KEY)
-}
-
-pub fn sequence_read(storage: &dyn Storage) -> ReadonlySingleton<u64> {
-    singleton_read(storage, SEQUENCE_KEY)
-}
-
-pub fn price_info(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
+pub fn price_info(storage: &mut dyn Storage) -> Bucket<PriceInfo> {
     bucket(storage, PRICE_INFO_KEY)
 }
 
-pub fn price_info_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
+pub fn price_info_read(storage: &dyn Storage) -> ReadonlyBucket<PriceInfo> {
     bucket_read(storage, PRICE_INFO_KEY)
 }
 

+ 248 - 0
terra/tools/deploy-pyth-bridge.js

@@ -0,0 +1,248 @@
+import { LCDClient, MnemonicKey } from "@terra-money/terra.js";
+import {
+  MsgInstantiateContract,
+  MsgMigrateContract,
+  MsgStoreCode,
+} from "@terra-money/terra.js";
+import { readFileSync } from "fs";
+import { Bech32, toHex } from "@cosmjs/encoding";
+import { zeroPad } from "ethers/lib/utils.js";
+import axios from "axios";
+import yargs from "yargs";
+import {hideBin} from "yargs/helpers";
+import assert from "assert";
+
+export const TERRA_GAS_PRICES_URL = "https://fcd.terra.dev/v1/txs/gas_prices";
+
+const argv = yargs(hideBin(process.argv))
+  .option('network', {
+    description: 'Which network to deploy to',
+    choices: ['mainnet', 'testnet'],
+    required: true
+  })
+  .option('artifact', {
+    description: 'Path to Pyth artifact',
+    type: 'string',
+    required: false
+  })
+  .option('mnemonic', {
+    description: 'Mnemonic (private key)',
+    type: 'string',
+    required: true
+  })
+  .option('instantiate', {
+    description: 'Instantiate contract if set (default: disabled)',
+    type: 'boolean',
+    default: false,
+    required: false
+  })
+  .option('migrate', {
+    description: 'Migrate an existing contract if set (default: disabled)',
+    type: 'boolean',
+    default: false,
+    required: false
+  })
+  .option('contract', {
+    description: 'Contract address, used only for migration',
+    type: 'string',
+    required: false,
+    default: ''
+  })
+  .option('code-id', {
+    description: 'Code Id, if provided this will be used for migrate/instantiate and no code will be uploaded',
+    type: 'number',
+    requred: false
+  })
+  .help()
+  .alias('help', 'h').argv;
+
+const artifact = argv.artifact;
+
+/* Set up terra client & wallet. It won't fail because inputs are validated with yargs */
+
+const CONFIG = {
+  mainnet: {
+    terraHost: {
+      URL: "https://lcd.terra.dev",
+      chainID: "columbus-5",
+      name: "mainnet",
+    },
+    wormholeContract: "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5",
+    pythEmitterAddress: "6bb14509a612f01fbbc4cffeebd4bbfb492a86df717ebe92eb6df432a3f00a25"
+  },
+  testnet: {
+    terraHost: {
+      URL: "https://bombay-lcd.terra.dev",
+      chainID: "bombay-12",
+      name: "testnet",
+    },
+    wormholeContract: "terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v",
+    pythEmitterAddress: "f346195ac02f37d60d4db8ffa6ef74cb1be3550047543a4a9ee9acf4d78697b0"
+  }
+}
+
+const terraHost = CONFIG[argv.network].terraHost;
+const wormholeContract = CONFIG[argv.network].wormholeContract;
+const pythEmitterAddress = CONFIG[argv.network].pythEmitterAddress;
+  
+const lcd = new LCDClient(terraHost);
+
+const feeDenoms = ["uluna"];
+
+const gasPrices = await axios
+  .get(TERRA_GAS_PRICES_URL)
+  .then((result) => result.data);
+
+const wallet = lcd.wallet(
+  new MnemonicKey({
+    mnemonic: argv.mnemonic
+  })
+);
+
+/* Deploy artifacts */
+
+var codeId; 
+
+if (argv.codeId !== undefined) {
+  codeId = argv.codeId;
+} else {
+  if (argv.artifact === undefined) {
+    console.error("Artifact is not provided. Please at least provide artifact or code id");
+    process.exit(1);
+  }
+
+  const contract_bytes = readFileSync(artifact);
+  console.log(`Storing WASM: ${artifact} (${contract_bytes.length} bytes)`);
+
+  const store_code = new MsgStoreCode(
+    wallet.key.accAddress,
+    contract_bytes.toString("base64")
+  );
+
+  const feeEstimate = await lcd.tx.estimateFee(
+    wallet.key.accAddress,
+    [store_code],
+    {
+      feeDenoms,
+      gasPrices,
+    }
+  );
+
+  console.log("Deploy fee: ", feeEstimate.amount.toString());
+
+  const tx = await wallet.createAndSignTx({
+    msgs: [store_code],
+    feeDenoms,
+    gasPrices,
+    fee: feeEstimate,
+  });
+
+  const rs = await lcd.tx.broadcast(tx);
+
+  try {
+    const ci = /"code_id","value":"([^"]+)/gm.exec(rs.raw_log)[1];
+    codeId = parseInt(ci);
+  } catch(e) {
+    console.error("Encountered an error in parsing deploy code result. Printing raw log")
+    console.error(rs.raw_log);
+    throw(e);
+  }
+
+  console.log("Code ID: ", codeId);
+
+  if (argv.instantiate || argv.migrate) {
+    console.log("Sleeping for 10 seconds for store transaction to finalize.");
+    await sleep(10000);
+  }
+}
+
+if (argv.instantiate) {
+  console.log("Instantiating a contract");
+
+  async function instantiate(codeId, inst_msg) {
+    var address;
+    await wallet
+      .createAndSignTx({
+        msgs: [
+          new MsgInstantiateContract(
+            wallet.key.accAddress,
+            wallet.key.accAddress,
+            codeId,
+            inst_msg
+          ),
+        ],
+      })
+      .then((tx) => lcd.tx.broadcast(tx))
+      .then((rs) => {
+        try {
+          address = /"contract_address","value":"([^"]+)/gm.exec(rs.raw_log)[1];
+        } catch (e) { 
+          console.error("Encountered an error in parsing instantiation result. Printing raw log")
+          console.error(rs.raw_log);
+          throw(e);
+        }
+      });
+    console.log(`Instantiated Pyth Bridge at ${address} (${convert_terra_address_to_hex(address)})`);
+    return address;
+  }
+
+  const pythChain = 1;
+
+  const contractAddress = await instantiate(codeId, {
+    wormhole_contract: wormholeContract,
+    pyth_emitter: Buffer.from(pythEmitterAddress, "hex").toString(
+      "base64"
+    ),
+    pyth_emitter_chain: pythChain,
+  });
+
+  console.log(`Deployed pyth contract at ${contractAddress}`);
+}
+
+if (argv.migrate) {
+  if (argv.contract === '') {
+    console.error("Contract address is not provided. Provide it using --contract");
+    process.exit(1);
+  }
+
+  console.log(`Migrating contract ${argv.contract} to ${codeId}`);
+
+  const tx = await wallet.createAndSignTx({
+    msgs: [
+      new MsgMigrateContract(
+        wallet.key.accAddress,
+        argv.contract,
+        codeId,
+        {
+          "action": ""
+        },
+        { uluna: 1000 }
+      ),
+    ],
+    feeDenoms,
+    gasPrices,
+  });
+  
+  const rs = await lcd.tx.broadcast(tx);
+  var resultCodeId;
+  try {
+    resultCodeId = /"code_id","value":"([^"]+)/gm.exec(rs.raw_log)[1];
+    assert.equal(codeId, resultCodeId)
+  } catch (e) {
+    console.error("Encountered an error in parsing migration result. Printing raw log")
+    console.error(rs.raw_log);
+    throw(e);
+  }
+
+  console.log(`Contract ${argv.contract} code_id successfully updated to ${resultCodeId}`);
+}
+
+// Terra addresses are "human-readable", but for cross-chain registrations, we
+// want the "canonical" version
+function convert_terra_address_to_hex(human_addr) {
+  return "0x" + toHex(zeroPad(Bech32.decode(human_addr).data, 32));
+}
+
+function sleep(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}

+ 2 - 1
terra/tools/package.json

@@ -5,7 +5,8 @@
   "main": "deploy.js",
   "type": "module",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "deploy-pyth": "node deploy-pyth-bridge.js"
   },
   "author": "",
   "license": "ISC",

+ 59 - 5
third_party/pyth/p2w-sdk/rust/Cargo.lock

@@ -311,6 +311,12 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "dyn-clone"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28"
+
 [[package]]
 name = "either"
 version = "1.6.1"
@@ -565,9 +571,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "p2w-sdk"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
- "pyth-client",
+ "pyth-sdk-solana",
  "serde",
  "solana-program",
  "solitaire",
@@ -624,16 +630,29 @@ dependencies = [
 ]
 
 [[package]]
-name = "pyth-client"
-version = "0.5.0"
+name = "pyth-sdk"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f779e98b8c8016d0c1409247a204bd4fcdea8b67ceeef545f04e324d66c49e52"
+checksum = "c610102a39fc4bae29a3b5a628ee134d25afb3dca3937692f5e634f1287fe0b4"
+dependencies = [
+ "borsh",
+ "borsh-derive",
+ "schemars",
+ "serde",
+]
+
+[[package]]
+name = "pyth-sdk-solana"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1fdc94592a28fa829b0d6fa619392b1a1744048e5b78a74a4ba93cf541eddae"
 dependencies = [
  "borsh",
  "borsh-derive",
  "bytemuck",
  "num-derive",
  "num-traits",
+ "pyth-sdk",
  "serde",
  "solana-program",
  "thiserror",
@@ -748,6 +767,30 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
 
+[[package]]
+name = "schemars"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
@@ -789,6 +832,17 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "serde_derive_internals"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "serde_json"
 version = "1.0.79"

+ 2 - 2
third_party/pyth/p2w-sdk/rust/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "p2w-sdk"
-version = "0.1.0"
+version = "0.1.1"
 authors = ["Wormhole Contributors <contact@certus.one>"]
 edition = "2018"
 description = "Pyth to Wormhole SDK"
@@ -15,7 +15,7 @@ wasm = ["wasm-bindgen", "solana"]
 
 [dependencies]
 serde = { version = "1.0.103", default-features = false, features = ["derive"] }
-pyth-sdk-solana = { version = "0.1.0" }
+pyth-sdk-solana = { version = "0.2.0" }
 wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true}
 solitaire = { path = "../../../../solana/solitaire/program", optional = true }
 solana-program = "1.8.16"

+ 2 - 0
third_party/pyth/p2w-terra-relay/.dockerignore

@@ -0,0 +1,2 @@
+/lib
+/node_modules

+ 1 - 1
third_party/pyth/p2w-terra-relay/Dockerfile.pyth_relay

@@ -9,7 +9,7 @@ RUN npm install && npm run build && npm cache clean --force
 
 RUN mkdir -p /app/pyth_relay/logs
 RUN addgroup -S pyth -g 10001 && adduser -S pyth -G pyth -u 10001
-RUN chown -R pyth:pyth src/ logs/ lib/
+RUN chown -R pyth:pyth .
 USER pyth
 
 CMD [ "node", "lib/index.js" ]