Oracle for Algorand Using Smart Contracts
Overview
Smart contracts are powerful instruments to build useful and complex applications on the Algorand blockchain. One of the main drawbacks of smart contracts is that they are only aware of data located on the blockchain. Unlike regular web applications, they cannot directly connect to external data sources. Of course, you can send input parameters when you call the smart contract. But sometimes, you need more official data, that does not depend on the caller, but rather on an official and trusted party.
Consider an interesting case. Suppose that you have a smart contract deployed on Algorand. The contract defines some behaviors that depend on fiat settlements to occur. Once you have confirmation from your bank that a settlement was successful, you want your smart contract to execute some business operations.
This is a common use case for Oracles. The goal of a blockchain Oracle is to provide external data to smart contracts running on the blockchain. In the following steps, we will see how to implement an Oracle on Algorand.
We expect you to be familiar with Algorand stateful smart contract written in PyTEAL, running the Algorand sandbox or using a third party service like Purestake to connect to the Algorand network. We will also use IntelliJ Community Edition and the AlgoDea plugin to write and deploy the smart contracts and to code and run a Java application.
The big picture
A good drawing is generally better than a lengthy and wordy description. Before diving deep into the details of our implementation, the following picture will give you a general idea of what we are going to build.
A company developing on Algorand has deployed a smart contract Oracle which provides access to currency exchange rates. One of their clients needs the EUR/USD exchange rate to process some business operations.
-
The client calls the smart contract and provides the following parameters:
getCurrenciesExchangeRate: This is the operation that the client wants to execute. It provides access to currency exchange rates.
EUR/USD: This is the currency pair the client desires.
10: This is the smart contract ID to which the oracle will provide the requested value.
getMarketExchangeCallback: This is the first argument that the oracle will provide when calling smart contract 10. -
The client call is successful and included in block #50 as Txn2 on Algorand.
-
The Algorand Oracle company has deployed a Java application, running in the cloud, which searches for all transactions sent to the smart contract. The application knows exactly which kind of transactions to search for and regularly queries an Algorand Indexer running on a node.
-
Once a transaction matching the criteria has been identified, it is processed by the Java application. In the current case, the application will retrieve the EUR/USD exchange rate from a third party. This is exactly what the smart contract cannot do by itself.
-
The currency exchange rate is then sent back to the smart contract whose application ID has been provided by the client in the original call. It is up to the smart contract to do whatever it wants with that value.
This was an overview of the big picture to how Oracles function. Now we are going to dive into the technical part, starting with the oracle PyTEAL smart contract.
The oracle smart contract
We start by the declaration of global variables used later in the contract.
from pyteal import *
ADMIN_KEY = Bytes("admin")
WHITELISTED_KEY = Bytes("whitelisted")
REQUESTS_BALANCE_KEY = Bytes("requests_balance")
MAX_BUY_AMOUNT = Int(1000000000)
MIN_BUY_AMOUNT = Int(10000000)
REQUESTS_SELLER = Addr("N5ICVTFKS7RJJHGWWM5QXG2L3BV3GEF6N37D2ZF73O4PCBZCXP4HV3K7CY")
MARKET_EXCHANGE_NOTE = Bytes("algo-oracle-app-4")
Then the approval program starts by handling the initialization of the smart contract. We mark the account used to create the contract as the admin
.
def approval_program():
on_creation = Seq(
[
Assert(Txn.application_args.length() == Int(0)),
App.localPut(Int(0), ADMIN_KEY, Int(1)),
Return(Int(1))
]
)
Depending on your needs, you may also want to have some special operations that only administrators can do. The code below is meant to be called by an admin
to grant/revoke the admin
role to other accounts.
is_contract_admin = App.localGet(Int(0), ADMIN_KEY)
# set/remove an admin for this contract
admin_status = Btoi(Txn.application_args[2])
set_admin = Seq(
[
Assert(
And(
is_contract_admin,
Txn.application_args.length() == Int(3),
Txn.accounts.length() == Int(1),
)
),
App.localPut(Int(1), ADMIN_KEY, admin_status),
Return(Int(1)),
]
)
The register
function below is called when someone OptIn
to the smart contract.
I generally use a whitelisting mechanism when dealing with smart contract. Whitelisting is a way to make sure that only authorized accounts can call your smart contract. It also allows you to have off-chain onboarding process (like KYC) before granting access to your contract. When an account OptIn, I mark him as “not whitelisted”. As shown in the following code, an admin account needs to whitelist an account after it has OptIn our application.
register = Seq(
[
App.localPut(Int(0), WHITELISTED_KEY, Int(0)), Return(Int(1))
]
)
# Depending on what you do, you should always consider implementing a whitelisting to
# control who access your app. This will allow you to process offchain validation before
# allowing an account to call you app.
# You may also consider case by case whitelisting to allow access to specific business methods.
whitelist = Seq(
[
Assert(
And(
is_contract_admin,
Txn.application_args.length() == Int(2),
Txn.accounts.length() == Int(1)
)
),
App.localPut(Int(1), WHITELISTED_KEY, Int(1)),
Return(Int(1))
]
)
# This should be added to the checklist of business methods.
is_whitelisted = App.localGet(Int(0), WHITELISTED_KEY)
The following code allows a client to buy a bunch of requests. The work of Algorand Oracle company is not free. Before using the service, you need to buy some requests. We decided to sell 10 oracle requests for 1 ALGO. To achieve that, you need to create an atomic transfer
, i.e a group of 2 transactions. The first one must be an ALGO payment to the company’s account. The company does not transact for less than 10 ALGO or more than 1000 ALGO in value per transaction.
# a client can buy requests.
# buying requests must be done using an atomic transfer.
# the first transaction must be a payment to our address.
# we don't sell for less than 10 or more than 1000 ALGO.
# call to the contract is the second transaction.
# the account that will use the requests must be provided.
buy_requests = Seq(
[
Assert(
And(
is_whitelisted,
Global.group_size() == Int(2),
Gtxn[0].type_enum() == TxnType.Payment,
Gtxn[0].receiver() == REQUESTS_SELLER,
Gtxn[0].amount() >= MIN_BUY_AMOUNT,
Gtxn[0].amount() <= MAX_BUY_AMOUNT,
Txn.group_index() == Int(1),
Txn.application_args.length() == Int(2),
Txn.accounts.length() == Int(1)
)
),
App.localPut(
Int(1),
REQUESTS_BALANCE_KEY,
App.localGet(Int(1), REQUESTS_BALANCE_KEY) + (Gtxn[0].amount() / Int(100000)),
),
Return(Int(1))
]
)
Once you have enough credits, you can request a currency exchange rate.
It is mandatory for the client to provide the expected note in the transaction, otherwise it will not be processed by the oracle. As of the day of writing this article, the only way to query Algorand Indexer for all transactions send to a smart contract is by using the transaction note, see Indexer.
It is advisable to perform as many assertions and checks before accepting a transaction. Here we do not apply too much checks on the received arguments. But we will see later on with the Java application that we implemented more strict checks there.
market_exchange_rate_request = Seq(
[
Assert(
And(
is_whitelisted,
Txn.note() == MARKET_EXCHANGE_NOTE,
Txn.application_args.length() == Int(4),
Txn.accounts.length() == Int(0),
App.localGet(Int(0), REQUESTS_BALANCE_KEY) >= Int(1)
)
),
App.localPut(
Int(0),
REQUESTS_BALANCE_KEY,
App.localGet(Int(0), REQUESTS_BALANCE_KEY) - Int(1),
),
Return(Int(1))
]
)
And now, this is where the approval program will select which code to run. (the allocate_request
code has not been shown in this tutorial. It allows admin
account to increase the requests credit of another account. You can check it in the github repository )
program = Cond(
[Txn.application_id() == Int(0), on_creation],
[Txn.on_completion() == OnComplete.DeleteApplication, Return(is_contract_admin)],
[Txn.on_completion() == OnComplete.UpdateApplication, Return(is_contract_admin)],
[Txn.on_completion() == OnComplete.CloseOut, Return(Int(1))],
[Txn.on_completion() == OnComplete.OptIn, register],
[Txn.application_args[0] == Bytes("set_admin"), set_admin],
[Txn.application_args[0] == Bytes("whitelist"), whitelist],
[Txn.application_args[0] == Bytes("allocate_requests"), allocate_requests],
[Txn.application_args[0] == Bytes("buy_requests"), buy_requests],
[Txn.application_args[0] == Bytes("get_market_exchange_rate"), market_exchange_rate_request]
)
return program
We now have the standard clear state program.
def clear_state_program():
program = Seq(
[
Return(Int(1))
]
)
return program
Finally, we have the code that will compile the pyteal contract to teal.
if __name__ == "__main__":
with open("algorand_oracle_approval.teal", "w") as f:
compiled = compileTeal(approval_program(), mode=Mode.Application, version=5)
f.write(compiled)
with open("algorand_oracle_clear_state.teal", "w") as f:
compiled = compileTeal(clear_state_program(), mode=Mode.Application, version=5)
f.write(compiled)
There is a second contract in the github repository. It is the callback contract. But it is a rather simple contract that only needs to be “callable” with the callback argument provided by the client in the oracle request. Here is an abstract of the code of this contract that will be called by the Java application:
market_exchange_rate = Btoi(Txn.application_args[2]) # Value must be provided in micro algo (i.e 1 is 0.000001)
market = Txn.application_args[1]
get_market_exchange_rate_callback = Seq(
[
Assert(
And(
is_whitelisted,
Txn.application_args.length() == Int(3),
Txn.accounts.length() == Int(0),
)
),
# Do whatever you want with the market value. Here we store it in the global storage.
# It could be used later by other business methods
App.globalPut(market, market_exchange_rate),
Return(Int(1))
]
)
We are now ready to describe the Java application, that uses the Algorand Java SDK and Indexer to track transactions sent to the smart contract.
The Java application
The second piece of the oracle is the Java application. I used Java because I am a Java developer. But the code can be easily be adapted to any backend language.
I used Spring Boot, a famous framework in the Java world that allows you to quickly build an application, and the Algorand Java SDK.
There are 2 main components in the Java app: The component responsible for querying the Algorand Indexer to search for new transactions sent to request oracle information; and the component that processes the callback to the smart contract selected by the client, sending the requested value along.
Querying Algorand Indexer
We have created a Service, which is responsible for searching requests sent to the oracle. Given the fact that a new block is created on Algorand blockchain almost every 4 to 5 seconds, we have scheduled the service to run every 4.5 seconds. With Java Spring framework around, it is as simple as using the @Scheluled annotation
package com.example.algorand.oracle.requestsprocessor.application;
import com.algorand.algosdk.v2.client.common.AlgodClient;
import com.algorand.algosdk.v2.client.common.IndexerClient;
import com.algorand.algosdk.v2.client.common.Response;
import com.algorand.algosdk.v2.client.indexer.SearchForTransactions;
import com.algorand.algosdk.v2.client.model.Enums;
import com.algorand.algosdk.v2.client.model.Transaction;
import com.algorand.algosdk.v2.client.model.TransactionsResponse;
import com.example.algorand.oracle.requestsprocessor.domain.SupportedMarket;
import com.example.algorand.oracle.requestsprocessor.exceptions.AlgorandNetworkException;
import com.example.algorand.oracle.requestsprocessor.utils.AlgorandHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class OracleRequestsLoader {
private static byte[] NOTE_PREFIX = "algo-oracle-app-4".getBytes(StandardCharsets.UTF_8);
private final IndexerClient indexerClient;
private final AlgodClient algodClient;
private final OracleService oracleService;
@Value("${algorand.contract.oracle.id}")
private Long applicationId;
@Scheduled(fixedDelay = 4500)
public void triggerRequestLoading() {
log.info("Starting loading oracle requests from blockchain");
Long lastRound = oracleService.getLastProcessedRound();
log.info("Last round processed was {}", lastRound);
lastRound = loadOracleRequestsSentToApplicationFromRound(lastRound + 1);
log.info("Loaded new requests until round {}", lastRound);
oracleService.updateLastProcessedRound(lastRound);
}
public long loadOracleRequestsSentToApplicationFromRound( long round) {
String nextToken = "";
long lastIndexedRound = 0;
try {
lastIndexedRound = this.indexerClient.makeHealthCheck().execute().body().round;
} catch (Exception e) {
throw new AlgorandNetworkException("could not get last indexed round", e);
}
while (nextToken != null) {
SearchForTransactions searchForTransactions = this.indexerClient.searchForTransactions()
.notePrefix(NOTE_PREFIX)
.minRound(round)
.maxRound(lastIndexedRound);
if (nextToken != null) {
searchForTransactions.next(nextToken);
}
Response<TransactionsResponse> response = null;
try {
response = searchForTransactions.execute();
} catch (Exception e) {
throw new AlgorandNetworkException("An unexpected error occured while trying to read transactions on the blockchain", e);
}
if (!response.isSuccessful()) {
throw new AlgorandNetworkException(response.message());
}
TransactionsResponse transactionsResponse = response.body();
List<Transaction> oracleRequests = transactionsResponse.transactions.stream()
.filter(this::isSupportedTransaction)
.collect(Collectors.toList());
oracleService.createOracleRequests(oracleRequests);
if (StringUtils.isNotBlank(transactionsResponse.nextToken)) {
nextToken = transactionsResponse.nextToken;
} else {
nextToken = null;
}
}
return lastIndexedRound;
}
private boolean isSupportedTransaction(Transaction transaction) {
boolean supported =
// the transaction is a noop application call
transaction.txType.equals(Enums.TxType.APPL)
&& transaction.applicationTransaction.onCompletion.equals(Enums.OnCompletion.NOOP)
// we check that the application called is our smart contract
&& transaction.applicationTransaction.applicationId.equals(applicationId)
// we expect 4 arguments
&& transaction.applicationTransaction.applicationArgs().size() == 4
// the first one must be "get_market_exchange_rate"
&& AlgorandHelper.decodeToString(transaction.applicationTransaction.applicationArgs.get(0)).equals("get_market_exchange_rate")
// the second one must be a currencies pair supported by the oracle
&& SupportedMarket.fromMarketName(AlgorandHelper.decodeToString(transaction.applicationTransaction.applicationArgs.get(1))) != null
// the third one should be the callback application id (a long value)
&& AlgorandHelper.toLong(transaction.applicationTransaction.applicationArgs.get(2)) != null
//and finally, we expect a String value to send back later as first argument to the callback application
&& StringUtils.isNotBlank(AlgorandHelper.decodeToString(transaction.applicationTransaction.applicationArgs.get(3)));
log.info("Found " + (supported ? "": "not" ) + " supported transaction {}", transaction);
return supported;
}
}
Some comments describing the above code.
-
When the service is triggered, we start by fetching the value of the most recent round (
lastRound
variable above) we last checked to find any matching transactions. This value is retrieved from a database. For the sake of simplicity, we use the memory database H2. But for real application, you would use a production ready database. -
The query sent to the Indexer will filter transactions by including only those in the range
[lastRound+1, lastIndexedRound]
and those having the notealgo-oracle-app-4
(ThelastIndexedRound
value will later be saved in the database for the next check). You should use a note value with a high discriminating power to make sure you do not accidentally fetch undesired transactions. Here I selected a rather simple value, but for production level code, you should create a hash value (like SHA256) and send it to your client to use it as note. -
For each found transaction, it is mandatory to check that they are valid. The
isSupportedTransaction
does that. Please check the comment above each line of code in that private method. This is where you have to think a lot when writing production level code. A naive approach would be to assume that the note was enough to identify the transaction. But anyone can see your transaction note on a blockchain and therefore add it to a random transaction they create. The Java application would then be filled with spam transactions. So be careful! -
Once we found a valid request, we save it to the database. Why not process it immediately? It is generally good practice to separate responsibility when creating a production application. The goal of the OracleRequestsLoader service is to find requests sent to the oracle smart contract and store them. A separate component will then process each request.
Sending back the requested value to the blockchain
Now we have identified a transaction matching our criteria. We fetched the request value from a data source (we have access to a currency exchange market API) and want to send the pair rate back to the Algorand blockchain.
package com.example.algorand.oracle.requestsprocessor.application;
import com.algorand.algosdk.account.Account;
import com.algorand.algosdk.transaction.SignedTransaction;
import com.algorand.algosdk.transaction.Transaction;
import com.algorand.algosdk.util.Encoder;
import com.algorand.algosdk.v2.client.common.AlgodClient;
import com.algorand.algosdk.v2.client.common.Response;
import com.algorand.algosdk.v2.client.model.Application;
import com.algorand.algosdk.v2.client.model.PendingTransactionResponse;
import com.algorand.algosdk.v2.client.model.PostTransactionsResponse;
import com.algorand.algosdk.v2.client.model.TransactionParametersResponse;
import com.example.algorand.oracle.requestsprocessor.domain.OracleRequest;
import com.example.algorand.oracle.requestsprocessor.domain.OracleResponse;
import com.example.algorand.oracle.requestsprocessor.domain.TransactionProcessingStatus;
import com.example.algorand.oracle.requestsprocessor.exceptions.AlgorandCallbackProcessingException;
import com.example.algorand.oracle.requestsprocessor.exceptions.AlgorandNetworkException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@RequiredArgsConstructor
@Slf4j
public class OracleRequestsProcessor implements Runnable {
private final OracleRequest request;
private final AlgodClient algodClient;
private final OracleService oracleService;
private final Account callbackSender;
@Override
public void run() {
checkApplicationValidity();
Integer exchangeRate = randomExchangeRate(); // should be expressed in micro algo
List<byte[]> callbackArgs = buildCallbackArguments(exchangeRate);
try {
TransactionParametersResponse suggestedParams = algodClient.TransactionParams().execute().body();
Transaction callbackTransaction = Transaction.ApplicationCallTransactionBuilder()
.sender(callbackSender.getAddress())
.suggestedParams(suggestedParams)
.applicationId(request.getCallbackApplicationId())
.args(callbackArgs)
.build();
SignedTransaction signedTransaction = callbackSender.signTransaction(callbackTransaction);
byte[] bytes = Encoder.encodeToMsgPack(signedTransaction);
Response<PostTransactionsResponse> execute = algodClient.RawTransaction().rawtxn(bytes).execute();
if (!execute.isSuccessful()) {
String errMsg = String.format("A problem occurred while trying to call back app %d %s",
request.getCallbackApplicationId(), execute);
setRequestFailedStatus(errMsg);
throw new AlgorandCallbackProcessingException(errMsg);
}
waitForConfirmation(execute.body().txId);
handleRequestSuccess(execute.body().txId, exchangeRate);
} catch (Exception e) {
throw new AlgorandCallbackProcessingException("and unexpected error occured ", e);
}
}
private List<byte[]> buildCallbackArguments(Integer exchangeRate) {
return Arrays.asList(
request.getCallbackMethod().getBytes(StandardCharsets.UTF_8),
request.getMarketRequested().getMarket().getBytes(StandardCharsets.UTF_8),
ByteBuffer.allocate(4).putInt(exchangeRate).array());
}
private void checkApplicationValidity() {
setRequestStatus(TransactionProcessingStatus.PROCESSING);
try {
Response<Application> getApplicationResponse = algodClient.GetApplicationByID(request.getCallbackApplicationId()).execute();
if (!getApplicationResponse.isSuccessful()) {
String errMsg = String.format("A problem occurred while trying to get application info for %d %s",
request.getCallbackApplicationId(), getApplicationResponse);
setRequestFailedStatus(errMsg);
throw new AlgorandCallbackProcessingException(errMsg);
}
} catch (Exception e) {
setRequestFailedStatus(e.getMessage());
throw new AlgorandCallbackProcessingException("Unable to get application info for " + request.getCallbackApplicationId(),
e);
}
}
private void setRequestFailedStatus(String errMsg) {
request.setProcessingErrorMessage(errMsg);
setRequestStatus(TransactionProcessingStatus.FAILED);
}
private void setRequestStatus(TransactionProcessingStatus status) {
request.setStatus(status);
request.setProcessTime(LocalDateTime.now());
oracleService.updateOracleRequest(request);
}
private void handleRequestSuccess(String txId, Integer exchangeRate) {
OracleResponse oracleResponse = oracleService.createOracleResponse(request, txId, exchangeRate);
request.setOracleResponseId(oracleResponse.getId());
setRequestStatus(TransactionProcessingStatus.DONE);
}
private Integer randomExchangeRate() {
return 1000000 + ((new Random().nextInt(100) + 1) * 1000);
}
public void waitForConfirmation(String txID) throws Exception {
Long lastRound = algodClient.GetStatus().execute().body().lastRound;
long waitUntilRound = lastRound + 10;
while (lastRound <= waitUntilRound) {
try {
// Check the pending transactions
Response<PendingTransactionResponse> pendingInfo = algodClient.PendingTransactionInformation(txID).execute();
if (pendingInfo.body().confirmedRound != null && pendingInfo.body().confirmedRound > 0) {
// Got the completed Transaction
log.info(
"Transaction " + txID + " confirmed in round " + pendingInfo.body().confirmedRound);
break;
}
lastRound++;
algodClient.WaitForBlock(lastRound).execute();
} catch (Exception e) {
throw (e);
}
}
if (lastRound > waitUntilRound) {
throw new AlgorandNetworkException("The transaction " + txID + " could not be confirmed");
}
}
}
-
OracleRequestsProcessor
implements the Runnable interface, which in Java means that it can be executed in a separate thread. It is one of the core interfaces when dealing with multi-threading in Java. A new instance of this class will be created to process each request sent to the oracle. -
We start by checking that the application ID sent by the client is a valid one (it is the 3rd parameter in the client transaction application argument). If it does not exist, we log some error message end stop there.
-
Then, we generate a random value as the exchange rate that we will send back. Please, do not do this for real app! This is where you would fetch the real data instead.
-
Next, we prepare our callback by creating and signing a transaction, including the callback argument (the 4th argument send by the client) as the first argument.
-
Once successful, we mark the request as
DONE
in the database, with a timestamp. -
If any error occurs during the processing, we log it and stop the process.
Now, we need to see how the OracleRequestProcessor
task is run.
package com.example.algorand.oracle.requestsprocessor.application;
import com.algorand.algosdk.account.Account;
import com.algorand.algosdk.v2.client.common.AlgodClient;
import com.example.algorand.oracle.requestsprocessor.domain.TransactionProcessingStatus;
import com.example.algorand.oracle.requestsprocessor.repository.OracleRequestRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class OracleRequestsProcessingScheduler {
private final OracleRequestRepository oracleRequestRepository;
private final TaskExecutor oracleCallbackExecutor;
private final OracleService oracleService;
private final AlgodClient algodClient;
private final Account callbackSender;
@Scheduled(fixedDelay = 4500)
public void processRequests() {
oracleRequestRepository.findAllByStatus(TransactionProcessingStatus.CREATED)
.forEach(request -> {
log.info("going to process request {}", request.getId());
oracleCallbackExecutor.execute(
new OracleRequestsProcessor(request, algodClient, oracleService, callbackSender));
});
}
}
-
We have another service that is triggered every 4.5 seconds. It searches in the database for any request with status
CREATED
. This is the status that we assign to any newly stored request in the database. Statuses can be useful to monitor the requests. The service can create reports using the CREATE/DONE/FAILED requests. -
For each occurence found in the database, we create a new
OracleRequestsProcessor
that will process the request.
There are other classes that are not shown here. Please check the github repository algorand-oracle for the complete code source.
Deployment and testing
Deployment
I used the AlgoDea IntelliJ plugin to create and deploy the smart contracts on a local sandbox. You may prefer to use other tools. Anyway, make sure to at least perform the following operations described using goal commands.
- Install and run the local Algorand sandbox using:
git clone https://github.com/algorand/sandbox.git
cd sandbox
./sandbox up
- Create 3 accounts and fund them with some ALGOs using one of the default accounts available in the sandbox.
./sandbox goal account new Client
./sandbox goal account new OracleAdmin
./sandbox goal account new CallbackAdmin
./sandbox goal clerk send -a 1000000000 -f <use one of the default account address here> -t OracleAdmin
./sandbox goal clerk send -a 1000000000 -f <use one of the default account address here> -t Client
./sandbox goal clerk send -a 1000000000 -f <use one of the default account address here> -t CallbackAdmin
-
Update
algorand_oracle.py
and replace the value of the variableREQUESTS_SELLER
by the address ofOracleAdmin
. To see the addresses of the new created accounts, use./sandbox goal account list
. -
Generate the TEAL files corresponding to the smart contracts. 2 files are generated for each contract. To use TEAL files with the sandbox, you also need to copy them to the sandbox.
python3 algorand_oracle.py
python3 algorand_oracle_callback.py
./sandbox copyTo algorand_oracle_approval.teal
./sandbox copyTo algorand_oracle_clear_state.teal
./sandbox copyTo algorand_oracle_callback_approval.teal
./sandbox copyTo algorand_oracle_callback_clear_state.teal
- Deploy the
algorand_oracle
contract. We will consider that the deployed contract has application ID 10.
./sandbox goal app create --creator <OracleAdmin address> --approval-prog algorand_oracle_approval.teal --clear-prog algorand_oracle_clear_state.teal --local-ints 3 --on-completion OptIn --global-byteslices 0 --global-ints 0 --local-byteslices 0
- Deploy the
algorand_oracle_callback
contract. We will consider that the deployed contract has application ID 20. The--on-completion OptIn
argument is required because the local storage of the contract creator is used during deployment to set him as the admin.
./sandbox goal app create --creator <CallbackAdmin address> --approval-prog algorand_oracle_callback_approval.teal --clear-prog algorand_oracle_callback_clear_state.teal --local-ints 2 --on-completion OptIn --global-byteslices 0 --global-ints 1 --local-byteslices 0
- Make the
Client
accountOptIn
application 10 andOracleAdmin
accountOptIn
application 20.
./sandbox goal app optin --app-id 10 --from <Client address>
./sandbox goal app optin --app-id 20 --from <OracleAdmin address>
- Whitelist
Client
on application 10 and whitelistOracleAdmin
on application 20.
./sandbox goal app call --app-id 10 --from <OracleAdmin address> --app-arg 'str:whitelist','addr:<Client address>' --app-account <Client address>
./sandbox goal app call --app-id 20 --from <CallbackAdmin address> --app-arg 'str:whitelist','addr:<OracleAdmin address>' --app-account <OracleAdmin address>
- Grant some requests credit to the
Client
account. This will allow him to use the oracle.
./sandbox goal app call --app-id 10 --from <OracleAdmin address> --app-arg 'str:allocate_requests','int:100','addr:<Client address>' --app-account <Client address>
- You can also use the
buy_requests
feature which requires the accountClient
to create an atomic transfer.
./sandbox goal clerk send -a 10000000 -t OracleAdmin -f Client -o buy_oracle_request_payment.tx
./sandbox goal app call --app-id 10 --from <Client address> --app-arg 'str:buy_requests','addr:<Client address>' --app-account <Client address> -o buy_oracle_request_call_app.tx
./sandbox copyFrom buy_oracle_request_payment.tx
./sandbox copyFrom buy_oracle_request_call_app.tx
cat buy_oracle_request_payment.tx buy_oracle_request_call_app.tx > buy_oracle_request.tx
./sandbox copyTo buy_oracle_request.tx
./sandbox goal clerk group -i buy_oracle_request.tx -o buy_oracle_request_group.tx
./sandbox goal clerk sign -i buy_oracle_request_group.tx -o buy_oracle_request_group_signed.tx
./sandbox goal clerk rawsend -f buy_oracle_request_group_signed.tx
Client
account has now enough credit to send a request to our oracle. The note on the transaction below can be customized, but you will have to updatealgorand_oracle.py
, regenerate the teal files, redeploy the contract and also update OracleRequestsLoader.java
./sandbox goal app call --app-id 10 --from <Client address> --app-arg 'str:get_market_exchange_rate','str:EUR/USD','int:20','str:get_market_exchange_rate_callback' --note algo-oracle-app-4
Your are done with the smart contract. Now you need to run the Java Application, which will fetch the previous transaction from the indexer and callback the deployed algorand_oracle_callback.py
contract.
Deployment with IntelliJ + AlgoDea plugin
Now we will see how to perform all the above with IntelliJ + AlgoDea plugin.
-
First, follow the Setup IntelliJ guide located at the end of this article.
-
Create the 3 accounts using AlgoDea plugin
-
Fund the 3 accounts. If you are using the Algorand Sandbox, you can use any of the predefined accounts in the Sandbox to transfer ALGO to the 3 created accounts. If you use Algorand Testnet, use the Testnet Faucet to get some ALGOs.
-
Copy the Pyteal contracts located in the Github repository to the
src
folder you your new created project. -
Compile the Pyteal contracts. Open
algorand_oracle.py
, right-click anywhere in the file and select “Compile Pyteal”.
Two teal files will be generated in the generated/src folder: algorand_oracle_approval.teal
and algorand_oracle_clear_state.teal
- Deploy the
AlgorandOracle
contract using accountOracleAdmin
Make sure to select optin
in the On-completion option. This is mandatory when your contract access local storage during contract creation (in our case, it is necessary to set the creator as admin).
Calling the smart contract
The application has been created. Now use account Client
to OpIn AlgorandOracle
Now use the OracleAdmin
account to whitelist the Client
account by calling the AlgorandOracle
application.
With all the above steps, you should normally feel more comfortable with the AlgoDea plugin. We will not review all the features of the plugin. This will require a dedicated tutorial. Nonetheless, we can see how the client will call the buy_requests
to pay for requests before calling the service provided by the oracle.
To get some credits for requests, the account Client
needs to send a atomic transfer containing 2 transactions:
* The first transaction is a payment of 10 <= amount
<= 1000 ALGO. We will use the AlgeDea plugin to create this transaction. We will not send it, but rather save it for later.
- The second transaction is a call to
AlgorandOracle
to execute thebuy_requests
service. Do not forget to also export the transaction instead of sending it to the blockchain
We are now ready to create the atomic transfer. Select Atomic transfer in the AlgoDea menu. Use the “Add” button to add both transactions created and exported earlier.
Then click the “Create group” button to create the atomic transfer. Now select each transaction and click on “Sign” to sign them. The plugin will automatically select the Client
to sign, since you used it to create both transactions. Click OK to send it.
Once successful, you can check the local state of the Client
account for application AlgorandOracle, to make sure that the requests credit has been granted. For 10 ALGO, it should have received 100 requests credit.
Request a Currency exchange rate
The account Client
will use its credit to request the exchange rate between EUR/USD currencies. If you followed the previous steps, it should be easy for you to create the corresponding call the AlgorandOracle
application. Remember that the transaction must include a note, for our Java application to find it. The following screenshot show how to do it.
That is all for this smart contract!
Run the Java application
Use IntelliJ to open the algorand-oracle-requests-processor
project from the Github repository. It is a Java project which uses Apache Maven for the build. Maven is necessary to build the JAR file if you do not want to run it with IntelliJ. If you use IntelliJ, Maven is already included so you do not have to install it.
I will not go into the detail of building a Maven project. Let’s perform the configuration necessary to run the Java application.
First you need to provide the correct values in the src/main/resources/application.properties
file. If you use the Algorand sandbox to run a local isolated node, you only need to update the algorand.contract.oracle.id
and algorand.account.oracle.callback.mnemonic
properties (Important: Do not commit a mnemonic to public repository!). For other Algorand node, also update the properties related to the algod and indexer node
Secondly, create a simple run configuration as shown below:
The application will start and log a lot of useful information. You can see for example the round number that were include in the range of search send to the Indexer. When a transaction matching the expected criteria is found, you will see something like this:
It means that the Java application has identified a request sent to the oracle smart contract. It will save it in the database. The Oracle will then process the request and send back the exchange rate to the callback application on the blockchain.
The last check you need to do is to read the global state of the AlgorandOracleCallback
application. The exchange rate sent has been stored in the global state. You should see it like this:
Setup IntelliJ
To test the Oracle, I used Intellij which is a well known and famous editor in the Java community. You can follow the link to install it.
Once installed, you can run it and install the plugins we will need for the Pyteal smart contracts. Follow the guide.
First, install “Python Community Edition”
Then install AlgoDea plugin
Next, create a new project
Thanks to the AlgoDea plugin, you have now access to the Stateful smart contract project type.
You can put whatever names you want in the “approval program” and “clear state program” options as long as it ends with .teal extension. Since we are not going to code in TEAL language directly, we will delete those files later.
Click Next and select your project name and location and click finish
The AlgoDea plugin is very powerful and requires a dedicated tutorial to show everything it can do. It will help you do almost anything you need as a smart contract developer. The next thing we need to do is to configure the node that the plugin will use to compile and deploy our contracts
Enter the node info. Whether you use Algorand Sandbox or Purestake or any other node provider, enter the required info (here we use Algorand Sandbox).
Click on ” Fetch Network info ” to check the connection. You should get the Genesis ID and Genesis hash values. Click OK.
You now need to tell AlgoDea plugin to use the node you just configured to compile and deploy your contracts.
Now, we need the last configuration steps that will allow AlgoDea to compile the Pyteal contracts to .teal files. For this, we need to add a Python SDK to our project and add the PyTEAL package to this SDK.
Click on File -> Project structure to add the Python SDK, from the Python Community Edition plugin we added a few steps above.
If you do not see a Python SDK in the list, select “Add SDK” -> “Python SDK”. Follow the steps to install a Python interpreter. Intellij will propose you to download Python from the web.
Once you have selected the Python SDK, click the “Edit” button. We now need to add the PyTEAL package that will compile Pyteal to .teal files
Click on install package.
Now you are ready to go. We can now write our oracle smart contract.