From 1fac1e16f2e1242facc8d5446df07534bd5a5d73 Mon Sep 17 00:00:00 2001 From: "yongen.loong" Date: Thu, 12 Sep 2024 18:20:47 +0800 Subject: [PATCH] docs: update aetherlink docs --- docs/tools/oracle/index.md | 232 +--------- .../oracle/quick-start/automation/index.md | 247 +++++++++++ .../oracle/quick-start/data-feeds/index.md | 409 ++++++++++++++++++ docs/tools/oracle/quick-start/vrf/index.md | 297 +++++++++++++ 4 files changed, 964 insertions(+), 221 deletions(-) create mode 100644 docs/tools/oracle/quick-start/automation/index.md create mode 100644 docs/tools/oracle/quick-start/data-feeds/index.md create mode 100644 docs/tools/oracle/quick-start/vrf/index.md diff --git a/docs/tools/oracle/index.md b/docs/tools/oracle/index.md index bcc0a5e..8c4e70a 100644 --- a/docs/tools/oracle/index.md +++ b/docs/tools/oracle/index.md @@ -4,231 +4,21 @@ title: Oracle description: Transfer tamper-proof data from off-chain to on-chain --- -## 1. Background +## 1. Overview -There is a growing need for decentralized applications (dApps) to access data feeds that are frequently updated, reliable, and secure. AetherLink Oracles is a leading oracle provider directly fulfilling this need. +AetherLink is a decentralized oracle network that enables smart contracts to securely interact with real-world data and external APIs. It provides highly reliable and tamper-proof data inputs, which are essential for executing smart contracts that rely on off-chain information. AetherLink is built with security in mind, using decentralized node operators to prevent single points of failure, ensuring data integrity through multiple data sources, and offering verifiable randomness for enhanced transparency. Additionally, AetherLink is designed to be highly scalable, supporting a wide range of use cases from decentralized finance (DeFi) to gaming and beyond. -## 2. Target +AetherLink offers several key products that enhance the functionality of smart contracts: -For developers of the AELF ecosystem, this article will provide a complete guide to accessing the AetherLink **VRF** and **Datafeeds** contracts. +1. 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. +2. VRF (Verifiable Random Function): AetherLink VRF delivers provably fair and verifiable random numbers, ensuring the randomness used in smart contracts is tamper-proof and transparent. This is particularly useful for gaming, lotteries, and any application that requires trusted randomness. +3. Automation: AetherLink Automation allows developers to automate smart contract functions by triggering actions based on specific events or conditions. This reduces the need for manual intervention and ensures contracts execute efficiently and reliably when certain criteria are met. +4. CCIP (Cross-Chain Interoperability Protocol): AetherLink's CCIP enables seamless communication and interaction between different blockchains, allowing smart contracts to operate across multiple networks. This interoperability expands the capabilities of decentralized applications, making them more versatile and powerful. -## 3. Operational +## 2. Purpose -### 3.1 Preparation +The go-to place for developers who want to learn how to build applications using AetherLink Contract. -#### 3.1.1 Import proto +## 3. What's Next? -First, you need to import oracle-related proto files into your contract csproj file. - -```xml - - - Protobuf\Proto\oracle_common_message.proto - - - - - Protobuf\Proto\oracle_contract.proto - - - - - Protobuf\Proto\request_interface.proto - - -``` - -You can find the latest proto files through the following link: - -- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_common_message.proto -- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_contract.proto -- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/request_interface.proto - -#### 3.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 - -```protobuf -import "aelf/core.proto"; -import "aelf/options.proto"; -import "request_interface.proto"; -import "google/protobuf/empty.proto"; -import "google/protobuf/wrappers.proto"; -import "google/protobuf/timestamp.proto"; - -service { - ... - - option (aelf.base) = "request_interface.proto"; - - ... -``` - -#### 3.1.3 Set Oracle Address - -Set the Oracle Contract Address in your contract state according to your operating environment. - -- MainNet **AELF** Address : `BGhrBNTPcLccaxPv6hHJrn4CHHzeMovTsrkhFse5o2nwfvQyG` -- MainNet **tDVV** Address : `BGhrBNTPcLccaxPv6hHJrn4CHHzeMovTsrkhFse5o2nwfvQyG` -- TestNet **AELF** Address : `21Fh7yog1B741yioZhNAFbs3byJ97jvBmbGAPPZKZpHHog5aEg` -- TestNet **tDVW** Address : `21Fh7yog1B741yioZhNAFbs3byJ97jvBmbGAPPZKZpHHog5aEg` - -#### 3.1.4 Apply subscription ID - -Before initiating a task to the oracle, you need to contact AetherLink to apply for a subscription number for your task. Later, you can manage the service fee based on this subscription id.Remember that you only have the right to use your own `subscription id`. - -:::note - -If you have prepared all the above work, we will explain the VRF and Datafeeds scenarios separately. You need to choose the appropriate contract as the entry point for your development scenario - -Currently, we provide two oracle capabilities: - -- VRF: VRF provides cryptographically secure randomness for your blockchain-based applications. -- Datafeeds: Provide data sources for Web2 asset prices for contracts. - -::: - -### 3.2 DataFeeds - -First, you need to define how to initiate a Datafeeds oracle request in the contract. - -```csharp -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 Name | Explanation | | -| ---------------- | ------------------------------------------------------------------ | --------------------------- | -| SubscriptionId | manage the service fee based on this subscription id | int32 | -| RequestTypeIndex | Task Type | int32, 1=Datafeeds \| 2=VRF | -| SpecificData | Detailed description of Datafeeds tasks | ByteString | -| TraceId | This ID can be used as a unique index to manage your oracle tasks. | Aelf.Hash | - -#### 3.2.1 How to generate DataFeeds SpecificData - -Here we take the example of collecting ELF-USTD currency price pairs every 10 minutes. Note the red part: - -```json -{ - "Cron": "0 */10 * * * ?", - "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. - -```csharp -var jobSpec = "{\"Cron\": \"0 */10 * * * ?\",\"DataFeedsJobSpec\": {\"Type\": \"PriceFeeds\",\"CurrencyPair\": \"ELF/USDT\"}}"; - -var specificData = new AetherLink.Contracts.DataFeeds.Coordinator.SpecificData -{ - Data = ByteString.CopyFromUtf8(jobSpec), - DataVersion = 0 -}.ToByteString() - -var sendRequestInput = new SendRequestInput -{ - SubscriptionId = 1, - RequestTypeIndex = 1, - SpecificData = specificData, - TraceId = HhfWg...Y9kao31 // HASH -} -``` - -#### 3.2.2 How to handle oracle pricefeeds callbacks - -Here you need to override the HandleOracleFulfillment method - -```csharp -public override Empty HandleOracleFulfillment(HandleOracleFulfillmentInput input) -{ - ... - - var priceList = LongList.Parser.ParseFrom(input.Response); - - var longList = new LongList { Data = { priceList.Data } }; - - var sortedList = longList.Data.ToList().OrderBy(l => l).ToList(); - - var round = State.LatestRound.Value.Add(1); - - var newPriceRoundData = new PriceRoundData - { - Price = sortedList[sortedList.Count / 2], - RoundId = round, - UpdatedAt = Context.CurrentBlockTime - }; - State.LatestPriceRoundData.Value = newPriceRoundData; - State.PriceRoundData[round] = newPriceRoundData; - State.LatestRound.Value = round; -} -``` - -The above code will parse the oracle callback and record each result in the contract, providing the latest and historical currency price query capabilities. - -### 3.3 VRF - -First, you need to define how to initiate a VRF oracle request in the contract. - -```csharp -State.OracleContract.SendRequest.Send(new SendRequestInput -{ - SubscriptionId = SubscriptionId, - RequestTypeIndex = 2, - SpecificData = specificData, - TraceId = XXXXX // HASH -}); -``` - -- `OracleContract`: This is the target contract address, the oracle contract. -- `SendRequest`: This is the method name for sending the VRF Request to the target contract. -- `SendRequestInput`: This is the input parameter of the method for sending transactions to the target contract - -| Param Name | Explanation | | -| ---------------- | ------------------------------------------------------------------ | --------------------------- | -| SubscriptionId | manage the service fee based on this subscription id | int32 | -| RequestTypeIndex | Task Type | int32, 1=Datafeeds \| 2=VRF | -| SpecificData | Detailed description of Datafeeds tasks | ByteString | -| TraceId | This ID can be used as a unique index to manage your oracle tasks. | Aelf.Hash | - -#### 3.3.1 How to generate VRF SpecificData - -First, you need to specify an oracle node to perform your random number generation task, - -```csharp -var keyHashs = State.OracleContract.GetProvingKeyHashes.Call(new Empty()); -var keyHash = keyHashs[0] -``` - -Then bind the oracle node Keyhash in your VRF task and specify the number of random numbers to be generated. - -```csharp -var specificData = new AetherLink.Contracts.VRF.Coordinator.SpecificData -{ - KeyHash = keyHash, - NumWords = 3, - RequestConfirmations = 1 -}.ToByteString() -``` - -#### 3.3.2 How to handle oracle pricefeeds callbacks - -```csharp -public override Empty HandleOracleFulfillment(HandleOracleFulfillmentInput input) -{ - var randomHashList = HashList.Parser.ParseFrom(input.Response); - State.RandomHashes[input.RequestId] = randomHashList; -} -``` +You have now understood the basic functions of Oracle. Next, you can choose the appropriate Oracle product based on your scenario to make your DApp contract more secure and efficient. diff --git a/docs/tools/oracle/quick-start/automation/index.md b/docs/tools/oracle/quick-start/automation/index.md new file mode 100644 index 0000000..dee0430 --- /dev/null +++ b/docs/tools/oracle/quick-start/automation/index.md @@ -0,0 +1,247 @@ +--- +sidebar_position: 3 +--- + +AetherLink Automation allows developers to automate smart contract functions by triggering actions based on specific events or conditions. This reduces the need for manual intervention and ensures contracts execute efficiently and reliably when certain criteria are met. + +## 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 links: + +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_common_message.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_contract.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/feature/automation/protobuf/upkeep_interface.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/feature/automation/protobuf/automation_contract.proto + +### 1.2 Protobuf file + +Then you also need to introduce `upkeep_interface.proto` in the proto file to inherit the oracle callback function to receive the oracle perform. + +```proto +syntax = "proto3"; + +package upkeep; + +import "aelf/core.proto"; +import "aelf/options.proto"; +import "upkeep_interface.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/timestamp.proto"; + +option csharp_namespace = "AetherLink.Contracts.AutomationDemo"; + +service AutomationDemoContract { + option (aelf.base) = "upkeep_interface.proto"; + option (aelf.csharp_state) = "AetherLink.Contracts.AutomationDemo.AutomationDemoContractState"; + + rpc BuyInvestment(BuyInvestmentInput) returns (google.protobuf.Empty) {} +} + +message BuyInvestmentInput { + aelf.Hash investment_name = 1; + int64 amount = 2; +} + +message InvestmentInfo { + aelf.Hash investment_name = 1; + aelf.Hash investment_price = 2; + string reward_currency_name = 3; + int64 daily_interest_rate = 4; +} + +message OrderRecord { + aelf.Address consumer = 1; + aelf.Hash investment_name = 2; + int64 amount = 3; + google.protobuf.Timestamp created = 4; +} + +message InvestmentBought { + option (aelf.is_event) = true; + aelf.Address consumer = 1; + aelf.Hash investment_name = 2; + aelf.Hash investment_price = 3; + string reward_currency_name = 4; + int64 amount = 5; +} + +message RewardsTransferred { + option (aelf.is_event) = true; + aelf.Address beneficiary = 1; + aelf.Hash investment_name = 2; + int64 amount = 3; +} + +message LogEventCreated { + option (aelf.is_event) = true; + aelf.Hash mock_data = 1; +} +``` + +## 2. Getting Started + +### 2.1 Background + +Here, we will use the scenario of a DeFi investment product as the background: After a user initiates a purchase of an ELF investment product, the DApp contract will transfer the corresponding ELF to the user as interest at an annual rate of 1%. Since on-chain contracts cannot execute timed operations, an oracle task will be used to securely trigger timed transactions through the oracle node, activating the transfer logic within the DApp contract. + +### 2.2 How to initiate an Automation request + +Creating an Automation task means registering yourself as an upkeep. + +```csharp +State.AutomationContract.RegisterUpkeep.Send(new RegisterUpkeepInput +{ + Name = $"{input.InvestmentName}-{Context.Sender}-{Context.CurrentBlockTime}", + UpkeepContract = Context.Self, + AdminAddress = State.Admin.Value, + TriggerType = TriggerType.Cron, + TriggerData = ByteString.CopyFromUtf8(CronJobSpec), + PerformData = record.ToByteString() +}); +``` + +#### Parameters + +| Name | Explanation | +| ---------------- | ------------------------------------------------------------------------------------------------------------------- | +| `Name` | The name of the upkeep, which can be used to distinguish different upkeeps and insert parameters such as blocktime. | +| `UpkeepContract` | Receive the contract address triggered by the oracle, fill in the contract address itself. | +| `AdminAddress` | Upkeep manager address, who has the management rights of Upkeep. | +| `TriggerType` | Trigger type, currently supports timing trigger and log trigger. | +| `TriggerData` | Specific trigger information. | +| `PerformData` | Expected data to be submitted when triggered. | + +### 2.3 How to generate Automation TriggerData? + +Here we take the timing trigger as an example. You can fill in the time interval you expect to trigger according to the crontab expression rules. + +```csharp +private const string CronJobSpec = + "{\"Cron\": \"0 0 0 1/1 * ? \",\"TriggerDataSpec\": {\"TriggerType\": \"Cron\"}}"; +var triggerData = ByteString.CopyFromUtf8(CronJobSpec); +``` + +Here are some examples for your reference: + +| Expectation | JobSpec | +| ----------- | ----------------------------------------------------------------------------- | +| EveryMinute | `{"Cron": "* * * * * ? * ","TriggerDataSpec": {"TriggerType": "Cron"}}` | +| EveryHour | `{"Cron": "* 0/1 * * * ? * ","TriggerDataSpec": {"TriggerType": "Cron"}}` | +| EveryDay | `{"Cron": "* 0/1 0/1 * * ? * ","TriggerDataSpec": {"TriggerType": "Cron"}}` | +| EveryWeek | `{"Cron": "* 0/1 0/1 1/1 * * * ","TriggerDataSpec": {"TriggerType": "Cron"}}` | + +### 2.4 How to handle automation callbacks + +First, you need to import the `upkeep_interface.proto` file and rewrite the `PerformUpkeep` method to receive callbacks. `PerformData` is what you specified when you created Upkeep, so you only need to deserialize it when receiving it. + +```csharp +public override Empty PerformUpkeep(PerformUpkeepInput input) +{ + var record = OrderRecord.Parser.ParseFrom(input.PerformData); + var consumer = record.Consumer; + var purchaseQuantity = State.OrderRecordMap[HashHelper.ComputeFrom(record)]; + var investmentInfo = State.InvestmentInfoMap[record.InvestmentName]; + var transferAmount = purchaseQuantity.Amount.Mul(investmentInfo.DailyInterestRate.Div(100)); + + State.TokenContract.Transfer.Send(new TransferInput + { + To = consumer, + Symbol = investmentInfo.RewardCurrencyName, + Amount = transferAmount + }); + + Context.Fire(new RewardsTransferred + { + Beneficiary = consumer, + InvestmentName = record.InvestmentName, + Amount = transferAmount + }); + + return new Empty(); +} +``` + +### 2.5 Complete code + +You can get the complete contract code from GitHub: https://github.com/AetherLinkProject/aetherLink-contracts/tree/feature/automation-upkeep/contract/AetherLink.Contracts.AutomationDemo + +```csharp +using AElf; +using AElf.Contracts.MultiToken; +using AElf.CSharp.Core; +using AElf.Sdk.CSharp; +using AetherLink.Contracts.Automation; +using AetherLink.Contracts.Upkeep; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; + +namespace AetherLink.Contracts.AutomationDemo; + +public class AutomationDemoContract : AutomationDemoContractContainer.AutomationDemoContractBase +{ + private const string CronJobSpec = + "{\"Cron\": \"0 0 0 1/1 * ? \",\"TriggerDataSpec\": {\"TriggerType\": \"Cron\"}}"; + + public override Empty PerformUpkeep(PerformUpkeepInput input) + { + var record = OrderRecord.Parser.ParseFrom(input.PerformData); + var consumer = record.Consumer; + var purchaseQuantity = State.OrderRecordMap[HashHelper.ComputeFrom(record)]; + var investmentInfo = State.InvestmentInfoMap[record.InvestmentName]; + var transferAmount = purchaseQuantity.Amount.Mul(investmentInfo.DailyInterestRate.Div(100)); + + State.TokenContract.Transfer.Send(new TransferInput + { + To = consumer, + Symbol = investmentInfo.RewardCurrencyName, + Amount = transferAmount + }); + + Context.Fire(new RewardsTransferred + { + Beneficiary = consumer, + InvestmentName = record.InvestmentName, + Amount = transferAmount + }); + + return new Empty(); + } + + public override Empty BuyInvestment(BuyInvestmentInput input) + { + var record = new OrderRecord + { + Consumer = Context.Sender, + InvestmentName = input.InvestmentName, + Amount = input.Amount, + Created = Context.CurrentBlockTime + }; + + State.AutomationContract.RegisterUpkeep.Send(new RegisterUpkeepInput + { + Name = $"{input.InvestmentName}-{Context.Sender}-{Context.CurrentBlockTime}", + UpkeepContract = Context.Self, + AdminAddress = State.Admin.Value, + TriggerType = TriggerType.Cron, + TriggerData = ByteString.CopyFromUtf8(CronJobSpec), + PerformData = record.ToByteString() + }); + + State.OrderRecordMap[HashHelper.ComputeFrom(record)] = record; + + Context.Fire(new InvestmentBought + { + Consumer = Context.Sender, + InvestmentName = input.InvestmentName, + InvestmentPrice = State.InvestmentInfoMap[input.InvestmentName].InvestmentPrice, + RewardCurrencyName = State.InvestmentInfoMap[input.InvestmentName].RewardCurrencyName, + Amount = input.Amount + }); + + return new Empty(); + } +} +``` diff --git a/docs/tools/oracle/quick-start/data-feeds/index.md b/docs/tools/oracle/quick-start/data-feeds/index.md new file mode 100644 index 0000000..d5d4c36 --- /dev/null +++ b/docs/tools/oracle/quick-start/data-feeds/index.md @@ -0,0 +1,409 @@ +# 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: + +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_common_message.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_contract.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/request_interface.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/data_feeds_coordinator_contract.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/coordinator_contract.proto + +### 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 + +```protobuf +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: + +```csharp +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 Name | Explanation | | +| ---------------- | ------------------------------------------------------------------ | --------------------------- | +| SubscriptionId | manage the service fee based on this `subscription id` | int32 | +| RequestTypeIndex | Task Type | int32, 1=Datafeeds \| 2=VRF | +| SpecificData | Detailed description of Datafeeds tasks | ByteString | +| TraceId | This 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. + +```csharp +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
Owner { get; set; } +public MappedState 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. + +```json +{ + "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. + +```csharp +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 + +```csharp +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. + +```csharp +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 + +```csharp +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(); + } + +} +``` diff --git a/docs/tools/oracle/quick-start/vrf/index.md b/docs/tools/oracle/quick-start/vrf/index.md new file mode 100644 index 0000000..04064dd --- /dev/null +++ b/docs/tools/oracle/quick-start/vrf/index.md @@ -0,0 +1,297 @@ +# VRF + +AetherLink VRF delivers provably fair and verifiable random numbers, ensuring the randomness used in smart contracts is tamper-proof and transparent. This is particularly useful for gaming, lotteries, and any application that requires trusted randomness. + +## 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 links: + +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_common_message.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/oracle_contract.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/request_interface.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/vrf_coordinator_contract.proto +- https://github.com/AetherLinkProject/aetherLink-contracts/blob/master/protobuf/coordinator_contract.proto + +### 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. + +```proto +syntax = "proto3"; + +package demo; + +import "aelf/core.proto"; +import "aelf/options.proto"; +import "acs12.proto"; +import "request_interface.proto"; + +// The namespace of this class +option csharp_namespace = "AElf.Contracts.VRFDemo"; + +service VRFDemoContract { + // 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.VRFDemo.VRFDemoContractState"; + + rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty) { } + rpc Play (google.protobuf.Int64Value) returns (google.protobuf.Empty) { } +} + +// An event that will be emitted from contract method call when Play is called. +message PlayOutcomeEvent { + option (aelf.is_event) = true; + int64 won = 1; +} + +message RecordInfo { + aelf.Address user_address = 1; + int64 play_amount = 2; +} +``` + +## 2. Getting Started + +### 2.1 Background + +Here, we will use the scenario of a guess-the-number game in a Game DApp as the background: After a user initiates a "Play" transaction, the DApp contract needs to generate a true random number and then determine its value. To achieve this, an oracle task will be used to generate a verifiable random number off-chain and submit it on-chain. Based on the result of this random number, the user will either receive rewards or have their bet deducted. + +### 2.2 How to initiate a VRF oracle request + +First, you need to define how to initiate a VRF oracle request in the contract. + +```csharp +State.OracleContract.SendRequest.Send(new SendRequestInput +{ + SubscriptionId = SubscriptionId, + RequestTypeIndex = 2, + SpecificData = specificData, + TraceId = XXXXX // HASH +}); +``` + +- **OracleContract**: This is the target contract address, the oracle contract. +- **SendRequest**: This is the method name for sending the VRF Request to the target contract. +- **SendRequestInput**: This is the input parameter of the method for sending transactions to the target contract. + +| Param Name | Explanation | Type | +| ---------------- | ------------------------------------------------------------------ | --------------------------- | +| SubscriptionId | Manage the service fee based on this subscription id | int32 | +| RequestTypeIndex | Task Type | int32, 1=Datafeeds \| 2=VRF | +| SpecificData | Detailed description of VRF tasks | ByteString | +| TraceId | This 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 `playedRecord` in the contract, using `traceId` as the key for the historical record, and storing the metadata of the historical record as the value. + +```csharp +using AElf.Sdk.CSharp.State; +using AElf.Types; + +namespace AElf.Contracts.aetherlink_demo +{ + // The state class is access the blockchain state + public partial class aetherlink_demoState : ContractState + { + // A state to check if contract is initialized + public BoolState Initialized { get; set; } + // A state to store the owner address + public SingletonState
Owner { get; set; } + + public MappedState PlayedRecords { get; set; } + } +} +``` + +### 2.4 How to generate VRF SpecificData + +First, you need to specify an oracle node to perform your random number generation task. + +```csharp +var keyHashs = State.OracleContract.GetProvingKeyHashes.Call(new Empty()); +var keyHash = keyHashs[0]; +``` + +Then bind the oracle node `KeyHash` in your VRF task and specify the number of random numbers to be generated. + +```csharp +var specificData = new AetherLink.Contracts.VRF.Coordinator.SpecificData +{ + KeyHash = keyHash, + NumWords = 1, + RequestConfirmations = 1 +}.ToByteString(); +``` + +| Params | Explanation | Type | +| -------------------- | ---------------------------------------- | ----- | +| KeyHash | Oracle Node Public Key Hash | Hash | +| NumWords | Number of Random Hashs Generated | int32 | +| RequestConfirmations | Number of Blocks to Wait for Transmitted | int32 | + +### 2.5 How to handle oracle vrf callbacks + +The length of the `HashList` depends on the `NumWords` you specified when creating the oracle task, which is the number of random hashes generated. Next, you can use this random hash for your random number game. + +```csharp +public override Empty HandleOracleFulfillment(HandleOracleFulfillmentInput input) +{ + var randomHashList = HashList.Parser.ParseFrom(input.Response); + ... +} +``` + +### 2.6 Complete code + +You can get the complete contract code from GitHub: https://github.com/AetherLinkProject/aetherLink-contracts/tree/feature/aetherlink-vrf-demo/contract/AetherLink.Contracts.VRFDemo + +```csharp +using AElf; +using AElf.Contracts.MultiToken; +using AElf.Contracts.VRFDemo; +using AElf.Sdk.CSharp; +using AElf.Types; +using AetherLink.Contracts.Consumer; +using AetherLink.Contracts.Oracle; +using AetherLink.Contracts.VRF.Coordinator; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; + +namespace AetherLink.Contracts.VRFDemo; + +public partial class VRFDemoContract : VRFDemoContractContainer.VRFDemoContractBase +{ + private const string OracleContractAddress = "21Fh7yog1B741yioZhNAFbs3byJ97jvBmbGAPPZKZpHHog5aEg"; // tDVW oracle contract address + private const string TokenSymbol = "ELF"; + private const long MinimumPlayAmount = 1_000_000; // 0.01 ELF + private const long MaximumPlayAmount = 1_000_000_000; // 10 ELF + private const long SubscriptionId = 1; // input your subscriptionId + + // 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 HandleOracleFulfillment(HandleOracleFulfillmentInput input) + { + var userRecord = State.PlayedRecords[input.TraceId]; + if (userRecord != null) return new Empty(); + var randomHashList = HashList.Parser.ParseFrom(input.Response); + var userAddress = userRecord.UserAddress; + var playAmount = userRecord.PlayAmount; + if (IsWinner(randomHashList.Data[0])) + { + State.TokenContract.Transfer.Send(new TransferInput + { + To = userAddress, + Symbol = TokenSymbol, + Amount = playAmount + }); + + Context.Fire(new PlayOutcomeEvent + { + Won = playAmount + }); + } + else + { + State.TokenContract.TransferFrom.Send(new TransferFromInput + { + From = userAddress, + To = Context.Self, + Symbol = TokenSymbol, + Amount = playAmount + }); + + Context.Fire(new PlayOutcomeEvent + { + Won = -playAmount + }); + } + + return new Empty(); + } + + public override Empty Play(Int64Value input) + { + var playAmount = input.Value; + Assert(playAmount is >= MinimumPlayAmount and <= MaximumPlayAmount, "Invalid play amount."); + var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput + { + Owner = Context.Sender, + Symbol = TokenSymbol + }).Balance; + Assert(balance >= playAmount, "Insufficient balance."); + + var contractBalance = State.TokenContract.GetBalance.Call(new GetBalanceInput + { + Owner = Context.Self, + Symbol = TokenSymbol + }).Balance; + Assert(contractBalance >= playAmount, "Insufficient contract balance."); + + var keyHashs = State.OracleContract.GetProvingKeyHashes.Call(new Empty()); + var keyHash = keyHashs.Data[0]; + var specificData = new SpecificData + { + KeyHash = keyHash, + NumWords = 1, + RequestConfirmations = 1 + }.ToByteString(); + + var request = new SendRequestInput + { + SubscriptionId = SubscriptionId, + RequestTypeIndex = 2, + SpecificData = specificData, + }; + + var traceId = HashHelper.ConcatAndCompute( + HashHelper.ConcatAndCompute(HashHelper.ComputeFrom(Context.CurrentBlockTime), + HashHelper.ComputeFrom(Context.Origin)), HashHelper.ComputeFrom(request)); + request.TraceId = traceId; + State.OracleContract.SendRequest.Send(request); + + State.PlayedRecords[traceId] = new() + { + UserAddress = Context.Sender, + PlayAmount = input.Value + }; + + return new Empty(); + } + + private bool IsWinner(Hash randomHash) + => int.Parse(randomHash.ToHex().Substring(0, 8), System.Globalization.NumberStyles.HexNumber) % 2 == 0; +} +``` + +### 2.7 Interact with Your Deployed Smart Contract + +#### 2.7.1 Playing the Lottery Game + +```bash +$ aelf-command send ${CONTRACT_ADDRESS} -a ${WALLET_ADDRESS} -p ${WALLET_PASSWORD} -e https://tdvw-test-node.aelf.io Play +``` + +- Wait for the off-chain oracle node to execute (approximately 3-9 seconds), then check your `balance`. + +```bash +$ aelf-command call ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx -a ${WALLET_ADDRESS} -p ${WALLET_PASSWORD} -e https://tdvw-test-node.aelf.io GetBalance +``` + +- You will be prompted for the following: + +```bash + - Enter the required param ``: ELF + - Enter the required param ``: $WALLET_ADDRESS +```