Skip to main content

DataFeeds

AetherLink Datafeeds provide decentralized, real-time data from multiple sources, enabling smart contracts to access highly reliable and accurate off-chain information, such as asset prices, for secure decision-making in decentralized applications.

1. Preparation

1.1 Import proto

First, you need to import oracle-related proto files into your contract project. You can find the latest proto files through the following link:

1.2 Protobuf file

Then you also need to introduce request_interface.proto in the proto file to inherit the oracle callback function to receive the oracle report

syntax = "proto3";

package demo;

import "aelf/core.proto";
import "aelf/options.proto";
import "acs12.proto";
import "request_interface.proto";

// import for using the google.protobuf.* type.
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";

// The namespace of this class
option csharp_namespace = "AElf.Contracts.DataFeedsDemo";

service DataFeedsDemoContract {
// The name of the state class the smart contract is going to use to access blockchain state
option (aelf.base) = "acs12.proto";
option (aelf.base) = "request_interface.proto";
option (aelf.csharp_state) = "AetherLink.Contracts.DataFeedsDemo.DataFeedsDemoContractState";

rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty) {}
rpc StartPriceCollection (google.protobuf.Empty) returns (google.protobuf.Empty) {}
rpc Purchase (PurchaseInput) returns (google.protobuf.Empty) {}
}

message LongList {
repeated int64 data = 1;
}

message PriceData {
int64 price = 1;
string token_pair = 2;
google.protobuf.Timestamp updated_at = 3;
}

message PurchaseInput {
string token_symbol_to_buy = 1;
int64 token_amount = 2;
Price sell_price = 3;
}

message Price {
aelf.Hash symbol = 1;
int64 amount = 2;
}

// log event
message PriceUpdated {
option (aelf.is_event) = true;
int64 price = 1;
string token_pair = 2;
google.protobuf.Timestamp update_at = 3;
}

2. Getting Started

2.1 Background

Here, we will use the scenario of selling NFTs in a DeFi DApp as the background: NFTs are listed for sale on the platform, and they are priced in USDT. Users can choose to pay with ELF tokens. In this case, the contract needs to know the real-time ELF-USDT exchange rate. Therefore, an oracle task will be used to securely submit the exchange rate from off-chain to the on-chain DApp contract.

2.2 How to initiate a Datafeeds oracle request

First, you need to specify the Oracle contract and send a SendRequest transaction to the contract. The transaction input is constructed as follows:

State.OracleContract.SendRequest.Send(new SendRequestInput
{
SubscriptionId = SubscriptionId,
RequestTypeIndex = 1,
SpecificData = specificData,
TraceId = XXXXX // HASH
});
  • OracleContract: This is the target contract address, the oracle contract.
  • SendRequest: This is the method name for sending the Datafeeds Request to the target contract.
  • SendRequestInput: This is the input parameter of the method for sending transactions to the target contract
Param NameExplanation
SubscriptionIdmanage the service fee based on this subscription idint32
RequestTypeIndexTask Typeint32, 1=Datafeeds | 2=VRF
SpecificDataDetailed description of Datafeeds tasksByteString
TraceIdThis ID can be used as a unique index to manage your oracle tasks.Aelf.Hash

2.3 Why TraceId?

First, you need to understand that an oracle task is an asynchronous execution process that goes from off-chain to on-chain, back to off-chain, and is finally submitted on-chain by the oracle node. Therefore, you need to store the traceId as an index in the first transaction, and then match it with the information in the second transaction. Here, we add a State called PriceDataMap in the contract. You can use traceId as the unique identifier of the currency pair, and store the price of the currency pair as the value.

using AElf.Contracts.DataFeedsDemo;
using AElf.Sdk.CSharp.State;
using AElf.Types;

namespace AetherLink.Contracts.DataFeedsDemo;

public partial class DataFeedsDemoContractState : ContractState
{
// A state to check if contract is initialized
public BoolState Initialized { get; set; }
// A state to store the owner address
public SingletonState<Address> Owner { get; set; }
public MappedState<Hash, PriceData> PriceDataMap { get; set; }
}

2.4 How to generate DataFeeds SpecificData

Here we take the example of collecting ELF-USTD currency price pairs every 1 minutes.

{
"Cron": "0 _/1 _ * _ ?",
"DataFeedsJobSpec": {
"Type": "PriceFeeds",
"CurrencyPair": "ELF/USDT"
}
}

After determining the task description, you need to convert it into a bystring type and then put it into the input parameter of the oracle request.

private const long ELFUSDTInitPrice = 600000000;
private const long SGRUSDTInitPrice = 600000000;
private const string ELFUSDTTokenPair = "ELF/USDT";
private const string SGRUSDTTokenPair = "SGR/USDT";
private const string ELFUSDTJobSpec = "{\"Cron\": \"0 _/1 \* \* _ ?\",\"DataFeedsJobSpec\": {\"Type\": \"PriceFeeds\",\"CurrencyPair\": \"ELF/USDT\"}}";
private const string SGRUSDTJobSpec = "{\"Cron\": \"0 \_/1 \* \* \* ?\",\"DataFeedsJobSpec\": {\"Type\": \"PriceFeeds\",\"CurrencyPair\": \"SGR/USDT\"}}";

public override Empty StartPriceCollection(Empty input)
{
#region Start elf-usdt price request

{
var elfSpecData = new AetherLink.Contracts.DataFeeds.Coordinator.SpecificData
{
Data = ByteString.CopyFromUtf8(ELFUSDTJobSpec),
DataVersion = 0
}.ToByteString();
var elfPriceRequestInput = new SendRequestInput
{
SubscriptionId = SubscriptionId,
RequestTypeIndex = RequestTypeIndex,
SpecificData = elfSpecData
};
var elfTraceId = HashHelper.ComputeFrom(elfPriceRequestInput);
elfPriceRequestInput.TraceId = elfTraceId;
State.OracleContract.SendRequest.Send(elfPriceRequestInput);
State.PriceDataMap[elfTraceId] = new() { Price = ELFUSDTInitPrice, TokenPair = ELFUSDTTokenPair };
}

#endregion

#region Start sgr-usdt price request

{
var sgrSpecData = new AetherLink.Contracts.DataFeeds.Coordinator.SpecificData
{
Data = ByteString.CopyFromUtf8(SGRUSDTJobSpec),
DataVersion = 0
}.ToByteString();
var sgrPriceRequestInput = new SendRequestInput
{
SubscriptionId = SubscriptionId,
RequestTypeIndex = RequestTypeIndex,
SpecificData = sgrSpecData
};
var sgrTraceId = HashHelper.ComputeFrom(sgrPriceRequestInput);
sgrPriceRequestInput.TraceId = sgrTraceId;
State.OracleContract.SendRequest.Send(sgrPriceRequestInput);
State.PriceDataMap[sgrTraceId] = new() { Price = SGRUSDTInitPrice, TokenPair = SGRUSDTTokenPair };
}

#endregion

return new Empty();
}

2.5 How to handle oracle pricefeeds callbacks

Here you need to override the HandleOracleFulfillment method

public override Empty HandleOracleFulfillment(HandleOracleFulfillmentInput input)
{
if (input.Response.IsNullOrEmpty()) return new Empty();
if (input.TraceId == null || State.PriceDataMap[input.TraceId] == null) return new Empty();
var priceList = LongList.Parser.ParseFrom(input.Response);
var longList = new LongList { Data = { priceList.Data } };
var sortedList = longList.Data.ToList().OrderBy(l => l).ToList();
var latestPrice = sortedList[sortedList.Count / 2];
State.PriceDataMap[input.TraceId].Price = latestPrice;

Context.Fire(new PriceUpdated
{
Price = latestPrice,
TokenPair = State.PriceDataMap[input.TraceId].TokenPair,
UpdateAt = Context.CurrentBlockTime
});

return new Empty();
}

2.6 How to use coin price

The price is calculated by dividing the USDT price of the NFT by the ELF/USDT currency pair, that is, the number of ELF Tokens required to be paid.

public override Empty Purchase(PurchaseInput input)
{
// The price of NFT assets, assuming it is 10U
var price = State.NftPrice.Value;

// Receive ELF paid by the buyer
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = ELFPaymentTokenName,
// 10 / 0.033 = 10U / (1U / 30ELF)
Amount = price.Amount.Div(State.PriceDataMap[price.Symbol].Price)
});

// Transfer NFT assets to buyers
State.TokenContract.Transfer.Send(new TransferInput
{
To = Context.Sender,
Symbol = input.TokenSymbolToBuy,
Amount = input.TokenAmount
});

return new Empty();
}

2.7 Complete code

You can get the complete contract code from github: https://github.com/AetherLinkProject/aetherLink-contracts/tree/feature/aetherlink-datafeeds-demo/contract/AetherLink.Contracts.DataFeedsDemo

using System.Linq;
using AElf;
using AElf.Contracts.DataFeedsDemo;
using AElf.Contracts.MultiToken;
using AElf.CSharp.Core;
using AElf.Sdk.CSharp;
using AElf.Types;
using AetherLink.Contracts.Consumer;
using AetherLink.Contracts.Oracle;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;

namespace AetherLink.Contracts.DataFeedsDemo;

public class DataFeedsDemoContract : DataFeedsDemoContractContainer.DataFeedsDemoContractBase
{
private const long ELFUSDTInitPrice = 600000000;
private const long SGRUSDTInitPrice = 600000000;
private const string ELFUSDTTokenPair = "ELF/USDT";
private const string SGRUSDTTokenPair = "SGR/USDT";
private const string ELFPaymentTokenName = "ELF";
private const long SubscriptionId = 1; // input your subscriptionId
private const int RequestTypeIndex = 1;

private const string
OracleContractAddress = "21Fh7yog1B741yioZhNAFbs3byJ97jvBmbGAPPZKZpHHog5aEg"; // tDVW oracle contract address

private const string ELFUSDTJobSpec =
"{\"Cron\": \"0 */1 * * * ?\",\"DataFeedsJobSpec\": {\"Type\": \"PriceFeeds\",\"CurrencyPair\": \"ELF/USDT\"}}";

private const string SGRUSDTJobSpec =
"{\"Cron\": \"0 */1 * * * ?\",\"DataFeedsJobSpec\": {\"Type\": \"PriceFeeds\",\"CurrencyPair\": \"SGR/USDT\"}}";

// Initializes the contract
public override Empty Initialize(Empty input)
{
Assert(State.Initialized.Value == false, "Already initialized.");
State.Initialized.Value = true;
State.TokenContract.Value = Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);
State.OracleContract.Value = Address.FromBase58(OracleContractAddress);
return new Empty();
}

public override Empty StartPriceCollection(Empty input)
{
#region Start elf-usdt price request

{
var elfSpecData = new AetherLink.Contracts.DataFeeds.Coordinator.SpecificData
{
Data = ByteString.CopyFromUtf8(ELFUSDTJobSpec),
DataVersion = 0
}.ToByteString();
var elfPriceRequestInput = new SendRequestInput
{
SubscriptionId = SubscriptionId,
RequestTypeIndex = RequestTypeIndex,
SpecificData = elfSpecData
};
var elfTraceId = HashHelper.ComputeFrom(elfPriceRequestInput);
elfPriceRequestInput.TraceId = elfTraceId;
State.OracleContract.SendRequest.Send(elfPriceRequestInput);
State.PriceDataMap[elfTraceId] = new() { Price = ELFUSDTInitPrice, TokenPair = ELFUSDTTokenPair };
}

#endregion

#region Start sgr-usdt price request

{
var sgrSpecData = new AetherLink.Contracts.DataFeeds.Coordinator.SpecificData
{
Data = ByteString.CopyFromUtf8(SGRUSDTJobSpec),
DataVersion = 0
}.ToByteString();
var sgrPriceRequestInput = new SendRequestInput
{
SubscriptionId = SubscriptionId,
RequestTypeIndex = RequestTypeIndex,
SpecificData = sgrSpecData
};
var sgrTraceId = HashHelper.ComputeFrom(sgrPriceRequestInput);
sgrPriceRequestInput.TraceId = sgrTraceId;
State.OracleContract.SendRequest.Send(sgrPriceRequestInput);
State.PriceDataMap[sgrTraceId] = new() { Price = SGRUSDTInitPrice, TokenPair = SGRUSDTTokenPair };
}

#endregion

return new Empty();
}

public override Empty HandleOracleFulfillment(HandleOracleFulfillmentInput input)
{
if (input.Response.IsNullOrEmpty()) return new Empty();
if (input.TraceId == null || State.PriceDataMap[input.TraceId] == null) return new Empty();
var priceList = LongList.Parser.ParseFrom(input.Response);
var longList = new LongList { Data = { priceList.Data } };
var sortedList = longList.Data.ToList().OrderBy(l => l).ToList();
var latestPrice = sortedList[sortedList.Count / 2];
State.PriceDataMap[input.TraceId].Price = latestPrice;

Context.Fire(new PriceUpdated
{
Price = latestPrice,
TokenPair = State.PriceDataMap[input.TraceId].TokenPair,
UpdateAt = Context.CurrentBlockTime
});

return new Empty();
}

// transfer nft
public override Empty Purchase(PurchaseInput input)
{
// The price of NFT assets, assuming it is 10U
var price = State.NftPrice.Value;

// Receive ELF paid by the buyer
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = ELFPaymentTokenName,
// 10 / 0.033 = 10U / (1U / 30ELF)
Amount = price.Amount.Div(State.PriceDataMap[price.Symbol].Price)
});

// Transfer NFT assets to buyers
State.TokenContract.Transfer.Send(new TransferInput
{
To = Context.Sender,
Symbol = input.TokenSymbolToBuy,
Amount = input.TokenAmount
});

return new Empty();
}

}