Skip to content

Data Streams - Streams Direct #1907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
184 changes: 184 additions & 0 deletions public/samples/DataStreams/ClientReportsVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import {Common} from "@chainlink/contracts/src/v0.8/llo-feeds/libraries/Common.sol";
import {IRewardManager} from "@chainlink/contracts/src/v0.8/llo-feeds/interfaces/IRewardManager.sol";
import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/interfaces/IVerifierFeeManager.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

/**
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES.
* DO NOT USE THIS CODE IN PRODUCTION.
*/

// Custom interfaces for IVerifierProxy and IFeeManager
interface IVerifierProxy {
/**
* @notice Verifies that the data encoded has been signed.
* correctly by routing to the correct verifier, and bills the user if applicable.
* @param payload The encoded data to be verified, including the signed
* report.
* @param parameterPayload Fee metadata for billing. In the current implementation,
* this consists of the abi-encoded address of the ERC-20 token used for fees.
* @return verifierResponse The encoded report from the verifier.
*/
function verify(
bytes calldata payload,
bytes calldata parameterPayload
) external payable returns (bytes memory verifierResponse);

/**
* @notice Verifies multiple reports in bulk, ensuring that each is signed correctly,
* routes them to the appropriate verifier, and handles billing for the verification process.
* @param payloads An array of encoded data to be verified, where each entry includes
* the signed report.
* @param parameterPayload Fee metadata for billing. In the current implementation,
* this consists of the abi-encoded address of the ERC-20 token used for fees.
* @return verifiedReports An array of encoded reports returned from the verifier.
*/
function verifyBulk(
bytes[] calldata payloads,
bytes calldata parameterPayload
) external payable returns (bytes[] memory verifiedReports);

function s_feeManager() external view returns (IVerifierFeeManager);
}

interface IFeeManager {
/**
* @notice Calculates the fee and reward associated with verifying a report, including discounts for subscribers.
* This function assesses the fee and reward for report verification, applying a discount for recognized subscriber addresses.
* @param subscriber The address attempting to verify the report. A discount is applied if this address
* is recognized as a subscriber.
* @param unverifiedReport The report data awaiting verification. The content of this report is used to
* determine the base fee and reward, before considering subscriber discounts.
* @param quoteAddress The payment token address used for quoting fees and rewards.
* @return fee The fee assessed for verifying the report, with subscriber discounts applied where applicable.
* @return reward The reward allocated to the caller for successfully verifying the report.
* @return totalDiscount The total discount amount deducted from the fee for subscribers.
*/
function getFeeAndReward(
address subscriber,
bytes memory unverifiedReport,
address quoteAddress
) external returns (Common.Asset memory, Common.Asset memory, uint256);

function i_linkAddress() external view returns (address);

function i_nativeAddress() external view returns (address);

function i_rewardManager() external view returns (address);
}

/**
* @dev This contract implements functionality to verify Data Streams reports from
* the Streams Direct API or WebSocket connection, with payment in LINK tokens.
*/
contract ClientReportsVerifier {
error NothingToWithdraw(); // Thrown when a withdrawal attempt is made but the contract holds no tokens of the specified type.
error NotOwner(address caller); // Thrown when a caller tries to execute a function that is restricted to the contract's owner.

struct Report {
bytes32 feedId; // The feed ID the report has data for
uint32 validFromTimestamp; // Earliest timestamp for which price is applicable
uint32 observationsTimestamp; // Latest timestamp for which price is applicable
uint192 nativeFee; // Base cost to validate a transaction using the report, denominated in the chain’s native token (WETH/ETH)
uint192 linkFee; // Base cost to validate a transaction using the report, denominated in LINK
uint32 expiresAt; // Latest timestamp where the report can be verified onchain
int192 price; // DON consensus median price, carried to 8 decimal places
int192 bid; // Simulated price impact of a buy order up to the X% depth of liquidity utilisation
int192 ask; // Simulated price impact of a sell order up to the X% depth of liquidity utilisation
}

IVerifierProxy public s_verifierProxy;

address private s_owner;
int192 public last_decoded_price;

event DecodedPrice(int192);

/**
* @param _verifierProxy The address of the VerifierProxy contract.
* You can find these addresses on https://docs.chain.link/data-streams/stream-ids
*/
constructor(address _verifierProxy) {
s_owner = msg.sender;
s_verifierProxy = IVerifierProxy(_verifierProxy);
}

/// @notice Checks if the caller is the owner of the contract.
modifier onlyOwner() {
if (msg.sender != s_owner) revert NotOwner(msg.sender);
_;
}

/**
* @notice Verifies a report and handles fee payment.
* @dev Decodes the unverified report, calculates fees, approves token spending, and verifies the report.
* Emits a DecodedPrice event upon successful verification and stores the price from the report in `last_decoded_price`.
* @param unverifiedReport The encoded report data to be verified.
*/
function verifyReport(bytes memory unverifiedReport) external {
// Report verification fees
IFeeManager feeManager = IFeeManager(
address(s_verifierProxy.s_feeManager())
);

IRewardManager rewardManager = IRewardManager(
address(feeManager.i_rewardManager())
);

(, /* bytes32[3] reportContextData */ bytes memory reportData) = abi
.decode(unverifiedReport, (bytes32[3], bytes));

address feeTokenAddress = feeManager.i_linkAddress();

(Common.Asset memory fee, , ) = feeManager.getFeeAndReward(
address(this),
reportData,
feeTokenAddress
);

// Approve rewardManager to spend this contract's balance in fees
IERC20(feeTokenAddress).approve(address(rewardManager), fee.amount);

// Verify the report
bytes memory verifiedReportData = s_verifierProxy.verify(
unverifiedReport,
abi.encode(feeTokenAddress)
);

// Decode verified report data into a Report struct
// If your report is a PremiumReport, you should decode it as a PremiumReport
Report memory verifiedReport = abi.decode(verifiedReportData, (Report));

// Log price from report
emit DecodedPrice(verifiedReport.price);

// Store the price from the report
last_decoded_price = verifiedReport.price;
}

/**
* @notice Withdraws all tokens of a specific ERC20 token type to a beneficiary address.
* @dev Utilizes SafeERC20's safeTransfer for secure token transfer. Reverts if the contract's balance of the specified token is zero.
* @param _beneficiary Address to which the tokens will be sent. Must not be the zero address.
* @param _token Address of the ERC20 token to be withdrawn. Must be a valid ERC20 token contract.
*/
function withdrawToken(
address _beneficiary,
address _token // LINK token address on Arbitrum Sepolia: 0x779877A7B0D9E8603169DdbD7836e478b4624789
) public onlyOwner {
// Retrieve the balance of this contract
uint256 amount = IERC20(_token).balanceOf(address(this));

// Revert if there is nothing to withdraw
if (amount == 0) revert NothingToWithdraw();

IERC20(_token).safeTransfer(_beneficiary, amount);
}
}
170 changes: 170 additions & 0 deletions public/samples/DataStreams/StreamsDirect/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// client.go

package client

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"

"github.com/ethereum/go-ethereum/common/hexutil"
)

type SingleReport struct {
FeedID hexutil.Bytes `json:"feedID"`
ValidFromTimestamp uint32 `json:"validFromTimestamp"`
ObservationsTimestamp uint32 `json:"observationsTimestamp"`
FullReport hexutil.Bytes `json:"fullReport"`
}

type SingleReportResponse struct {
Report SingleReport `json:"report"`
}

type BulkReportResponse struct {
Reports []SingleReport `json:"reports"`
}

const (
path = "/api/v1/reports"
bulkPath = "/api/v1/reports/bulk"
)

func GenerateHMAC(method string, path string, body []byte, clientId string, timestamp int64, userSecret string) string {
serverBodyHash := sha256.New()
serverBodyHash.Write(body)
serverBodyHashString := fmt.Sprintf("%s %s %s %s %d",
method,
path,
hex.EncodeToString(serverBodyHash.Sum(nil)),
clientId,
timestamp)
fmt.Println("Generating HMAC with the following: ", serverBodyHashString)
signedMessage := hmac.New(sha256.New, []byte(userSecret))
signedMessage.Write([]byte(serverBodyHashString))
userHmac := hex.EncodeToString(signedMessage.Sum(nil))
return userHmac
}

func GenerateAuthHeaders(method string, pathAndParams string, clientId string, userSecret string) http.Header {
header := http.Header{}
timestamp := time.Now().UTC().UnixMilli()
hmacString := GenerateHMAC(method, pathAndParams, []byte(""), clientId, timestamp, userSecret)

header.Add("Authorization", clientId)
header.Add("X-Authorization-Timestamp", strconv.FormatInt(timestamp, 10))
header.Add("X-Authorization-Signature-SHA256", hmacString)
return header
}

func FetchSingleReportSingleFeed(feedId string) (SingleReport, error) {
baseUrl := os.Getenv("BASE_URL") // Example: api.testnet-dataengine.chain.link
clientId := os.Getenv("CLIENT_ID") // Example: "00000000-0000-0000-0000-000000000000"
userSecret := os.Getenv("CLIENT_SECRET") // Example: "your-secret"

timestamp := time.Now().UTC().UnixMilli() - 500

params := url.Values{
"feedID": {feedId},
"timestamp": {fmt.Sprintf("%d", timestamp/1000)},
}

req := &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Scheme: "https",
Host: baseUrl,
Path: path,
RawQuery: params.Encode(),
},
}
req.Header = GenerateAuthHeaders(req.Method, req.URL.RequestURI(), clientId, userSecret)
fmt.Println("base: ", baseUrl)
fmt.Println("header: ", req.Header)
fmt.Println("params: ", params)

rawRes, err := http.DefaultClient.Do(req)
if err != nil {
return SingleReport{}, err
}
defer rawRes.Body.Close()

body, err := io.ReadAll(rawRes.Body)
if err != nil {
return SingleReport{}, err
}

if rawRes.StatusCode != http.StatusOK {
// Error messages are typically descriptive
return SingleReport{}, fmt.Errorf("unexpected status code %d: %v", rawRes.StatusCode, string(body))
}

var res SingleReportResponse
err = json.Unmarshal(body, &res)
if err != nil {
return SingleReport{}, err
}

return res.Report, nil
}

func FetchSingleReportManyFeeds(feedIds []string) ([]SingleReport, error) {
baseUrl := os.Getenv("BASE_URL") //Example: api.testnet-dataengine.chain.link
clientId := os.Getenv("CLIENT_ID") // Example: "00000000-0000-0000-0000-000000000000"
userSecret := os.Getenv("CLIENT_SECRET") // Example: "your-secret"

timestamp := time.Now().UTC().UnixMilli() - 500

params := url.Values{
"feedIDs": {strings.Join(feedIds, ",")},
"timestamp": {fmt.Sprintf("%d", timestamp/1000)},
}

req := &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Scheme: "https",
Host: baseUrl,
Path: bulkPath,
RawQuery: params.Encode(),
},
}

req.Header = GenerateAuthHeaders(req.Method, req.URL.RequestURI(), clientId, userSecret)
fmt.Println("base: ", baseUrl)
fmt.Println("header: ", req.Header)
fmt.Println("params: ", params)

rawRes, err := http.DefaultClient.Do(req)
if err != nil {
return []SingleReport{}, err
}
defer rawRes.Body.Close()

body, err := io.ReadAll(rawRes.Body)
if err != nil {
return []SingleReport{}, err
}

if rawRes.StatusCode != http.StatusOK {
// Error messages are typically descriptive
return []SingleReport{}, fmt.Errorf("unexpected status code %d: %v", rawRes.StatusCode, string(body))
}

var res BulkReportResponse
err = json.Unmarshal(body, &res)
if err != nil {
return []SingleReport{}, err
}

return res.Reports, nil
}
Loading