-
Notifications
You must be signed in to change notification settings - Fork 15
External App Tutorial
This tutorial will focus on how to create an app on top of OneLedger blockchain protocol. This app will be referred as "external app" in this tutorial.
Blockchain is a Distributed Ledger Technology (DLT), that abstracts away trust among 2 or more parties by introducing a technology that is unhackable, immutable and trusted. Any use case that involves trust between 2 or more parties, is where blockchain can be applied, and so it’s thought of regularly in Supply Chain and Logistics, Finance, Insurance, Litigation, Health Care and many others.
An external app will contains 4 layers in total to utilize the OneLedger blockchain network.
- Data Layer: stores data that will be used in the app
- Transaction Layer: any actions in the blockchain system will be executed as a transaction, each and every node in the blockchain network will run a transaction in the same order to reach a consensus state.
- RPC Layer: in order to interact with blockchain network, for example, trigger the transactions in transaction layer, or query data from data layer, we need RPC layer to be able to make RPC requests to block chain network.
- Block Function Layer: block function could contain the logic that can be excuted in the beginning of block or end of block.
💡Simply put, an external app will use different transactions to perform corresponding actions that deal with data in store(s) to achieve certain functionalities. We will also use block functions to run logic in the block beginner or block ender and provide rpc functions to support querying result from outside of blockchain.
- Pre Start
- Create your external app folder
- Create error codes package
- Create data package
- Create action package
- Create block function package
- Create RPC package
- Register the app into OneLedger blockchain
Ensure your system meets the following requirements:
Operating System must be one of the following:
- macOS
- Ensure Xcode Developer Tools is installed(only command line version is needed)
- A Debian-based Linux distribution
- Ubuntu is recommended for the smoothest install, otherwise you will have to set up your distribution for installing PPAs
- Go version 1.11 or higher
- git
Use which go
to check where Golang is installed
In your .bashrc
or .zshrc
file, set up environment variables as below:
export GOPATH="folder for all your Golang project"
export PATH=$PATH:$GOPATH/bin
export GO111MODULE=auto
export GOROOT="folder where Golang is installed"
export OLROOT="$GOPATH/src/github.com/Oneledger"
export OLTEST="$OLROOT/protocol/node/scripts"
export OLSCRIPT="$OLROOT/protocol/node/scripts"
export OLSETUP="$OLROOT/protocol/node/setup"
export OLDATA="$GOPATH/test"
and source it(choose .bashrc
or .zshrc
that you are using)
source ~/.bashrc
For Ubuntu:
sudo apt-get update
sudo apt install build-essential
sudo apt-get install libsnappy-dev
wget https://github.com/google/leveldb/archive/v1.20.tar.gz && \
tar -zxvf v1.20.tar.gz && \
cd leveldb-1.20/ && \
make && \
sudo cp -r out-static/lib* out-shared/lib* /usr/local/lib/ && \
cd include/ && \
sudo cp -r leveldb /usr/local/include/ && \
sudo ldconfig && \
rm -f v1.20.tar.gz
For MacOS:
brew install leveldb
cd $OLPATH
git clone github.com/Oneledger/protocol
cd ./protocol
git checkout develop
git checkout -b name-of-your-branch
make update
make install_c
If no error occurs, you are good to go.
You can use Goland or VScode. For Goland, it comes with a 30-day trial.
It takes 7 major steps to create an external app:
- Create your external app folder
- Create error codes package
- Create data package
- Create action(transaction) package
- Create block function package
- Create RPC package
- Register the app into OneLedger Blockchain
This tutorial will use an example app called "BID" to show the steps you need to take to build an external app.
This example app will provide the functionality of bidding on OneLedger Naming Service(ONS).
OneLedger Naming Service: The Domains created on the OneLedger Network can be tied directly to an Account (OneLedger Address) and can be associated to a website URLs, so that Businesses can connect their web properties to a OneLedger Account and send crypto-payments to any online business.
Inside protocol
folder, there is a folder named external_apps
, everything that needs to be done will be in this folder.
The structure inside external_apps
is shown in the below, which includes a common utility folder, an example project folder and an initialization file.
external_apps
├── bid(example project folder) <---
├── common(common utility folder)
└── init.go
The first step for external app is to create your own external app folder, just like bid
as the example.
Create error package and error codes file to define all the error codes used in your external app, you will be provided a range of error codes that are pre-allocated to this external app to avoid conflict.
All external app error codes will be a six digit number starts with 99, and for each external app, there are 100 error codes available. For example, 990100 to 990199.
All packages inside your app folder should follow the naming convention of app name + underscore + package name
, such as bid_error
.
external_apps
├── bid(example project folder)
│ └── bid_error
│ └── codes.go <---
├── common(common utility folder)
└── init.go
Later on you will be adding error codes into this file as below. (with your dedicated error codes)
const (
BidErrInvalidBidConvId = 990001
BidErrInvalidAsset = 990002
...
)
Data package takes care of the functionality to store, set, get and iterate data related to your external app. There will be data structs to represent single entry of data object, and there will be data stores to hold the data. You can use multiple data structs or stores if needed.
Data in all stores is saved in a non-relational key-value(leveldb) database universally, with different prefix in the key we can differentiate data entries in different stores.
external_apps
├── bid(example project folder)
│ ├── bid_data <---
│ └── bid_error
├── common(common utility folder)
└── init.go
Create a new file called types.go
in your data package.
external_apps
├── bid(example project folder)
│ ├── bid_data
│ │ └── types.go <---
│ └── bid_error
├── common(common utility folder)
└── init.go
This file will store all the basic types you will need in the app. This way they can be easily maintained in the future.
type (
BidConvId string
BidConvState int
BidAssetType int
BidConvStatus bool
BidOfferStatus int
BidOfferType int
BidOfferAmountStatus int
BidDecision bool
)
Create a new file called errors.go
external_apps
├── bid(example project folder)
│ ├── bid_data
│ │ ├── errors.go <---
│ │ └── types.go
│ └── bid_error
├── common(common utility folder)
└── init.go
Inside this file, we will define errors that potentially can be triggered in data layer.
var (
ErrInvalidBidConvId = codes.ProtocolError{bid_error.BidErrInvalidBidConvId, "invalid bid conversation id"}
ErrInvalidAsset = codes.ProtocolError{bid_error.BidErrInvalidAsset, "invalid bid asset"}
...
)
Here we define errors using the error code from bid_error
package in last step, and combine it with an error message.
Later on we can add more errors to this file.
external_apps
├── bid(example project folder)
│ ├── bid_data
│ │ ├── errors.go
│ │ ├── init.go <---
│ │ └── types.go
│ └── bid_error
├── common(common utility folder)
└── init.go
We will define some constants as below
const (
//Bid States
BidStateInvalid BidConvState = 0xEE
BidStateActive BidConvState = 0x01
BidStateSucceed BidConvState = 0x02
...
)
Since in the bid example app, we have two stores, and we define a master stores to hold both of them.
type BidMasterStore struct {
BidConv *BidConvStore
BidOffer *BidOfferStore
}
var _ data.ExtStore = &BidMasterStore{}
func (bm *BidMasterStore) WithState(state *storage.State) data.ExtStore {
bm.BidConv.WithState(state)
bm.BidOffer.WithState(state)
return bm
}
We need to implement WithState
method as above to impelent data.ExtStore
interface for our use. And we can use var _ data.ExtStore = &BidMasterStore{}
to check if the implementation is successful.
This part can be done in Define data stores that will be used in the external app if one store is enough for your app. If it's done in there, remember to implement
WithState
method as above.
As default, init
function is not needed in this part, but since the bid asset is a generic concept and allow any asset implement the BidAsset interface, here we need to register the concrete type of the asset in the init
function. And later we will use different serializer to perform (de)serialization.
This is the structs that you want to utilize to represent single entry of data object in the external app.
For example, this is a note if your app is a notebook, a product if your app is a product management system.
external_apps
├── bid(example project folder)
│ ├── bid_data
│ │ ├── errors.go
│ │ ├── init.go
│ │ ├── bid_conversation.go <---
│ │ └── types.go
│ └── bid_error
├── common(common utility folder)
└── init.go
In bid example app, we have two different data structs, bid conversation and bid offer. Let's use bid conversation as an example.
This bid_conversation.go
will at least contains a struct definition and a constructor as below.
type BidConv struct {
BidConvId BidConvId `json:"bidId"`
AssetOwner keys.Address `json:"assetOwner"`
AssetName string `json:"assetName"`
AssetType BidAssetType `json:"assetType"`
Bidder keys.Address `json:"bidder"`
DeadlineUTC int64 `json:"deadlineUtc"`
}
func NewBidConv(owner keys.Address, assetName string, assetType BidAssetType, bidder keys.Address, deadline int64, height int64) *BidConv {
return &BidConv{
BidConvId: generateBidConvID(owner.String()+assetName+bidder.String(), height),
AssetOwner: owner,
AssetName: assetName,
AssetType: assetType,
Bidder: bidder,
DeadlineUTC: deadline,
}
}
In the bid conversation, we have the bid conversation id to make each conversation unique.
Since this app is to let people bidding on an asset that belongs to an owner, we have owner and bidder here. The data type is OneLedger address.
🛠If your data object is an entity that designed to be owned by or traded/exchanged among users, you can use
keys.Address
as data type for that field. This represents an address on the OneLedger blockchain network. TheString()
method for address will return the string value for that address.
We also have bid asset name and type that will be used in the validation and exchange process. For example, this asset needs to be under owner's address, and it needs to be valid in the period of bidding.
The "height" here represents the block height, which is used as another factor to make this id unique.
This the storage struct that you want to store you data entries from above step.
In bid example app, we also have two different data store structs, bid conversation store and bid offer store. Let's use bid conversation store as an example.
external_apps
├── bid(example project folder)
│ ├── bid_data
│ │ ├── errors.go
│ │ ├── init.go
│ │ ├── bid_conversation.go
│ │ ├── bid_conversation_store.go <---
│ │ └── types.go
│ └── bid_error
├── common(common utility folder)
└── init.go
Inside bid_conversation_store.go
, we first define our data store
type BidConvStore struct {
state *storage.State
szlr serialize.Serializer
prefix []byte //Current Store Prefix
prefixActive []byte
prefixSucceed []byte
prefixRejected []byte
prefixCancelled []byte
prefixExpired []byte
prefixExpiredFailed []byte
}
todo explain state
-
state
is where this store is saved, as mentioned before, data in every store is saved in a key-value database, and this database exists in the state of block chain network. We need this state to put our data stores. -
szlr
is the serializer we used to handle (de)serialization -
prefix
is a list of prefix will be used to store data entries that you can choose per external app. The reason to separate data into different prefixs is to make the getting and iterating process more intuitive.- You can define your own prefix type and value as you want.
And we add the constructor for this store:
func NewBidConvStore(prefixActive string, prefixSucceed string, prefixCancelled string, prefixExpired string, prefixRejected string, state *storage.State) *BidConvStore {
return &BidConvStore{
state: state,
szlr: serialize.GetSerializer(serialize.LOCAL),
prefix: []byte(prefixActive),
prefixActive: []byte(prefixActive),
prefixSucceed: []byte(prefixSucceed),
prefixCancelled: []byte(prefixCancelled),
prefixExpired: []byte(prefixExpired),
prefixRejected: []byte(prefixRejected),
}
}
For serializer, normally we can just use serialize.PERSISTENT
, which will do the (de)serialization using JSON. As mentioned above, here serialize.LOCAL
is used to support generic bid asset in this example app.
Method GetState
and WithState
are used to correctly pass/get the state to/from the store.
func (bcs *BidConvStore) GetState() *storage.State {
return bcs.state
}
func (bcs *BidConvStore) WithState(state *storage.State) *BidConvStore {
bcs.state = state
return bcs
}
Method WithPrefixType
behaves like a filter, that will return the store with only selected prefix.
func (bcs *BidConvStore) WithPrefixType(prefixType BidConvState) *BidConvStore {
switch prefixType {
case BidStateActive:
bcs.prefix = bcs.prefixActive
case BidStateSucceed:
bcs.prefix = bcs.prefixSucceed
case BidStateRejected:
bcs.prefix = bcs.prefixRejected
case BidStateCancelled:
bcs.prefix = bcs.prefixCancelled
case BidStateExpired:
bcs.prefix = bcs.prefixExpired
}
return bcs
}
Method Set
is to set data into the store
func (bcs *BidConvStore) Set(bid *BidConv) error {
prefixed := append(bcs.prefix, bid.BidConvId...)
data, err := bcs.szlr.Serialize(bid)
if err != nil {
return ErrFailedInSerialization.Wrap(err)
}
err = bcs.state.Set(prefixed, data)
return ErrSettingRecord.Wrap(err)
}
Here we first append the bid conversation id as a part of key to the prefixed key, and serialize the bid conversation into the store. You can design your own key pattern.
When we design the key pattern, the most important point is to create every data entry with a unique key. In this example, this is achieved by letting bid conversation id be the hash of multiple factors.
Method Get
is to get data from the store
func (bcs *BidConvStore) Get(bidId BidConvId) (*BidConv, error) {
bid := &BidConv{}
prefixed := append(bcs.prefix, bidId...)
data, err := bcs.state.Get(prefixed)
if err != nil {
return nil, ErrGettingRecord.Wrap(err)
}
err = bcs.szlr.Deserialize(data, bid)
if err != nil {
return nil, ErrFailedInDeserialization.Wrap(err)
}
return bid, nil
}
First we create empty bid conversation object and construct our key, and get the corresponding data from the state. After this, we finally deserialize data into our object.
Method Exist
will be able to tell if a data entried can be found in the data store.
func (bcs *BidConvStore) Exists(key BidConvId) bool {
active := append(bcs.prefixActive, key...)
succeed := append(bcs.prefixSucceed, key...)
rejected := append(bcs.prefixRejected, key...)
cancelled := append(bcs.prefixCancelled, key...)
expired := append(bcs.prefixExpired, key...)
expiredFailed := append(bcs.prefixExpiredFailed, key...)
return bcs.state.Exists(active) || bcs.state.Exists(succeed) || bcs.state.Exists(rejected) || bcs.state.Exists(cancelled) || bcs.state.Exists(expired) || bcs.state.Exists(expiredFailed)
}
Here we check if it exists in stores with any our pre-defined prefixs.
Method Delete
is to delete data from the store
func (bcs *BidConvStore) Delete(key BidConvId) (bool, error) {
prefixed := append(bcs.prefix, key...)
res, err := bcs.state.Delete(prefixed)
if err != nil {
return false, ErrDeletingRecord.Wrap(err)
}
return res, err
}
Method Iterate
is to iterate all or part of data in the store to get what we want.
⚠️ Iterate
method can only be used in rpc package, not action package. Anything calls this method cannot be a part of action package.
func (bcs *BidConvStore) Iterate(fn func(id BidConvId, bid *BidConv) bool) (stopped bool) {
return bcs.state.IterateRange(
bcs.prefix,
storage.Rangefix(string(bcs.prefix)),
true,
func(key, value []byte) bool {
id := BidConvId(key)
bid := &BidConv{}
err := bcs.szlr.Deserialize(value, bid)
if err != nil {
return true
}
return fn(id, bid)
},
)
}
In this method, we will return the result from IterateRange
function in the state, we first pass the prefix of the store we want to iterate, and generate the range prefix from it, using assending direction when iterating.
And we use a function to get info from the key and value.
At the end we pass a customized function fn
into the Iterate
method, after we successfully get and deserialize the key and value for each data entry in the data store, we will call this fn
function to do the logic we want.
As an example, we call Iterate
method in FilterBidConvs
method:
func (bcs *BidConvStore) FilterBidConvs(bidState BidConvState, owner keys.Address, assetName string, assetType BidAssetType, bidder keys.Address) []BidConv {
prefix := bcs.prefix
defer func() { bcs.prefix = prefix }()
bidConvs := make([]BidConv, 0)
bcs.WithPrefixType(bidState).Iterate(func(id BidConvId, bidConv *BidConv) bool {
if len(owner) != 0 && !bidConv.AssetOwner.Equal(owner) {
return false
}
if len(bidder) != 0 && !bidConv.Bidder.Equal(bidder) {
return false
}
if bidConv.AssetType != assetType {
return false
}
if len(assetName) != 0 && !cmp.Equal(assetName, bidConv.AssetName) {
return false
}
bidConvs = append(bidConvs, *bidConv)
return false
})
return bidConvs
}
In this method, we pass a function that will check it the given query info is a match of the data entry that being iterated, and here return false means continue next round of iteration.
⚠️ Here we add thisdefer func() { bcs.prefix = prefix }()
to revert the change we made to the store. BecauseWithPrefixType
will re-point the store to a different child prefix.
If the key includes multiple infomation, we can use the iterate
method in bid_offer_store.go
as another example.
func (bos *BidOfferStore) iterate(fn func(bidConvId BidConvId, offerType BidOfferType, offerTime int64, offer BidOffer) bool) bool {
return bos.State.IterateRange(
assembleInactiveOfferPrefix(bos.prefix),
storage.Rangefix(string(assembleInactiveOfferPrefix(bos.prefix))),
true,
func(key, value []byte) bool {
offer := &BidOffer{}
err := serialize.GetSerializer(serialize.PERSISTENT).Deserialize(value, offer)
if err != nil {
return true
}
arr := strings.Split(string(key), storage.DB_PREFIX)
// key example: bidOffer_INACTIVE_bidConvId_offerType_offerTime
bidConvId := arr[2]
offerType, err := strconv.Atoi(arr[3])
if err != nil {
fmt.Println("Error Parsing Offer Type", err)
return true
}
offerTime, err := strconv.ParseInt(arr[len(arr)-1], 10, 64)
if err != nil {
fmt.Println("Error Parsing Offer Time", err)
return true
}
return fn(BidConvId(bidConvId), BidOfferType(offerType), int64(offerTime), *offer)
},
)
}
In bid offer section, we use offer status(ACTIVE/INACTIVE), related bid conversation id, offer type and offer time as key to make it unique.
And in the function where we get info from the key, we split the key into different components, which is separated by underscore.
Creating unit test is recommanded for data stores, it's better to fix the problem at data layer before going forward.
external_apps
├── bid(example project folder)
│ ├── bid_data
│ │ ├── errors.go
│ │ ├── init.go
│ │ ├── bid_conversation.go
│ │ ├── bid_conversation_store.go
│ │ ├── bid_conversation_store_test.go <---
│ │ └── types.go
│ └── bid_error
├── common(common utility folder)
└── init.go
In the bid_conversation_store_test.go
file, we need to setup the testing environment in init
function.
We first define some variables and constants that will be used in the test
const (
numPrivateKeys = 10
numBids = 10
testDeadline = 1596740920
height = 111
)
var (
addrList []keys.Address
bidConvs []*BidConv
bidConvStore *BidConvStore
assetNames []string
)
And next is to create an init
function to prepare the test
func init() {
fmt.Println("####### TESTING BID CONV STORE #######")
//Generate key pairs
for i := 0; i < numPrivateKeys; i++ {
pub, _, _ := keys.NewKeyPairFromTendermint()
h, _ := pub.GetHandler()
addrList = append(addrList, h.Address())
}
//Create new bid conversations
for i := 0; i < numBids; i++ {
j := i / 2 //owner address list ranges from 0 - 4
k := numPrivateKeys - 1 - j //bidder address list ranges from 9 - 5
owner := addrList[j]
assetNames = append(assetNames, "test"+strconv.Itoa(i)+".ol")
bidder := addrList[k]
bidConvs = append(bidConvs, NewBidConv(owner, assetNames[i], BidAssetOns,
bidder, testDeadline, height))
}
//Create Test DB
newDB := db.NewDB("test", db.MemDBBackend, "")
cs := storage.NewState(storage.NewChainState("chainstate", newDB))
//Create bid conversation store
bidConvStore = NewBidConvStore("p_active", "p_succeed", "p_cancelled", "p_expired", "p_rejected", cs)
}
In the init
function, we first create some addresses from key pairs, and create some bid conversations.
Next is to create a test db, as mentioned before, data in every store is saved in one key-value database universally, here we create the database and from which we get a chainstate.
Then we use the chainstate to create our store.
After the environment is set, we call our functions in the test to see if we can get expected results.
func TestBidConvStore_Set(t *testing.T) {
err := bidConvStore.Set(bidConvs[0])
assert.Equal(t, nil, err)
bidConv, err := bidConvStore.Get(bidConvs[0].BidConvId)
assert.Equal(t, nil, err)
assert.Equal(t, bidConv.BidConvId, bidConvs[0].BidConvId)
}
For example, here we set a data entry and try to get it.
And for iterate test, we need one more step
func TestBidConvStore_Iterate(t *testing.T) {
for _, val := range bidConvs {
_ = bidConvStore.Set(val)
}
bidConvStore.state.Commit()
bidConvCount := 0
bidConvStore.Iterate(func(id BidConvId, bidConv *BidConv) bool {
bidConvCount++
return false
})
assert.Equal(t, numBids, bidConvCount)
}
Before we try to iterate the store, we need to commit the current state using bidConvStore.state.Commit()
, this is the reason why we shouldn't use iterate method in action layer, if we do so, we may not get all the data we want.
Action pacakge handles all the transactions in the app. In the blockchain network, every action is achieved by a transaction, such as sending tokens to another address, creating a domain, adding data to stores...
When a fullnode in the blockchain network receives a transaction, it will first do basic validation, then it will be passed into the network. Since blockchain network is a decentralized system, the consensus established among all the nodes is essential.
After this transaction is passed into the network, it will be excuted in different nodes to achieve the consensus. That means EVERYTHING in the transaction should be deterministic, everything should follow the same step for one transaction in multiple nodes. No random number, no random sequence and so on.
external_apps
├── bid(example project folder)
│ ├── bid_action <---
│ ├── bid_data
│ └── bid_error
├── common(common utility folder)
└── init.go
external_apps
├── bid(example project folder)
│ ├── bid_action
│ │ └── init.go <---
│ ├── bid_data
│ └── bid_error
├── common(common utility folder)
└── init.go
todo confirm the action type code
Inside this init.go
, first we will define some constants as the action type. You will be provided with a range of three-digit action type codes in hex. For example 0x910
to 0x91F
.
const (
//Bid
BID_CREATE action.Type = 0x901
BID_CONTER_OFFER action.Type = 0x902
BID_CANCEL action.Type = 0x903
BID_BIDDER_DECISION action.Type = 0x904
BID_EXPIRE action.Type = 0x905
BID_OWNER_DECISION action.Type = 0x906
)
And we need to register our action types in the init
funtion.
func init() {
action.RegisterTxType(BID_CREATE, "BID_CREATE")
action.RegisterTxType(BID_CONTER_OFFER, "BID_CONTER_OFFER")
action.RegisterTxType(BID_CANCEL, "BID_CANCEL")
action.RegisterTxType(BID_BIDDER_DECISION, "BID_BIDDER_DECISION")
action.RegisterTxType(BID_EXPIRE, "BID_EXPIRE")
action.RegisterTxType(BID_OWNER_DECISION, "BID_OWNER_DECISION")
}
After this we register some bid assets into the bid asset map. This is only needed if your app has similar concept.
external_apps
├── bid(example project folder)
│ ├── bid_action
│ │ ├── create_bid.go <---
│ │ └── init.go
│ ├── bid_data
│ └── bid_error
├── common(common utility folder)
└── init.go
First let's create a file called create_bid.go
. Inside this file we will create one transaction.
As mentioned before, a transaction will be validated first when received by a fullnode, then broadcasted into the blockchain network. This requires the transaction to be written following a specific pattern.
There will be two objects for this transaction, remember to put correct json tag for deserialization.
type CreateBid struct {
BidConvId bid_data.BidConvId `json:"bidConvId"`
AssetOwner keys.Address `json:"assetOwner"`
AssetName string `json:"assetName"`
AssetType bid_data.BidAssetType `json:"assetType"`
Bidder keys.Address `json:"bidder"`
Amount action.Amount `json:"amount"`
Deadline int64 `json:"deadline"`
}
type CreateBidTx struct {
}
we use var _ action.Msg = &CreateBid{}
and var _ action.Tx = &CreateBidTx{}
to check if these two objects implement action.Msg
and action.Tx
interface separately. Right now there will be compiling errors saying the lack of methods, we will add them below.
Method Signer
will specify the signer of this transaction. Every transaction needs to be signed by an address, and the address needs to pay a small amount of fee in OLT token to the support the network. That means in the CreateBid
struct, there must be at least one parameter of keys.Address
type.
func (c CreateBid) Signers() []action.Address {
return []action.Address{c.Bidder}
}
Method Type
will return the action type we created for this transaction.
func (c CreateBid) Type() action.Type {
return BID_CREATE
}
Method Tag
will return a list of key-value pairs that contains some of the chosen parameters, this list will be included in the transaction events for future use.
func (c CreateBid) Tags() kv.Pairs {
tags := make([]kv.Pair, 0)
tag := kv.Pair{
Key: []byte("tx.bidConvId"),
Value: []byte(c.BidConvId),
}
tag1 := kv.Pair{
Key: []byte("tx.type"),
Value: []byte(c.Type().String()),
}
tag2 := kv.Pair{
Key: []byte("tx.assetOwner"),
Value: c.AssetOwner.Bytes(),
}
tag3 := kv.Pair{
Key: []byte("tx.asset"),
Value: []byte(c.AssetName),
}
tag4 := kv.Pair{
Key: []byte("tx.assetType"),
Value: []byte(strconv.Itoa(int(c.AssetType))),
}
tags = append(tags, tag, tag1, tag2, tag3, tag4)
return tags
}
Method Marshal
and Unmarshal
will provide the functionalities of (de)serialization in action layer.
func (c CreateBid) Marshal() ([]byte, error) {
return json.Marshal(c)
}
func (c *CreateBid) Unmarshal(bytes []byte) error {
return json.Unmarshal(bytes, c)
}
Method Validate
is needed to do basic validation when a fullnode receives the transaction. The receiver of this method is CreateBidTx
.
func (c CreateBidTx) Validate(ctx *action.Context, signedTx action.SignedTx) (bool, error) {
createBid := CreateBid{}
err := createBid.Unmarshal(signedTx.Data)
if err != nil {
return false, errors.Wrap(action.ErrWrongTxType, err.Error())
}
//validate basic signature
err = action.ValidateBasic(signedTx.RawBytes(), createBid.Signers(), signedTx.Signatures)
if err != nil {
return false, err
}
err = action.ValidateFee(ctx.FeePool.GetOpt(), signedTx.Fee)
if err != nil {
return false, err
}
// the currency should be OLT
currency, ok := ctx.Currencies.GetCurrencyById(0)
if !ok {
panic("no default currency available in the network")
}
if currency.Name != createBid.Amount.Currency {
return false, errors.Wrap(action.ErrInvalidAmount, createBid.Amount.String())
}
//Check if bid ID is valid(if provided)
if len(createBid.BidConvId) > 0 && createBid.BidConvId.Err() != nil {
return false, bid_data.ErrInvalidBidConvId
}
//Check if bidder and owner address is valid oneLedger address(if bid id is not provided)
if len(createBid.BidConvId) == 0 {
err = createBid.Bidder.Err()
if err != nil {
return false, errors.Wrap(action.ErrInvalidAddress, err.Error())
}
err = createBid.AssetOwner.Err()
if err != nil {
return false, errors.Wrap(action.ErrInvalidAddress, err.Error())
}
}
return true, nil
}
In the Validate
method, we first create an object of CreateBid
, and deserialize the transaction data into this object. You can reuse some errors here from action
package.
Then we validate basic signatures so that the we are sure this transaction is properly signed by an address.
After that, we need to make sure the currency for this transaction should be OLT.
At the end, we do some basic validation related to our app logic, such as the address validation.
⚠️ Do not do any complex validation that involves accessing chainstate or data store in theValidate
method, it will raise concurrency problem in the app.
Method ProcessFee
will process the amount of fee payed by the transaction signer.
func (c CreateBidTx) ProcessFee(ctx *action.Context, signedTx action.SignedTx, start action.Gas, size action.Gas) (bool, action.Response) {
return action.BasicFeeHandling(ctx, signedTx, start, size, 1)
}
Method ProcessCheck
and ProcessDeliver
represent different stage of including the transaction into the network. As mentioned before, to achieve consensus, a transaction will be excuted in different nodes.
todo why and what ProcessCheck
and ProcessDeliver
Inside these two methods, we will use function runCreateBid
to perform the actual transaction logic.
func runCreateBid(ctx *action.Context, tx action.RawTx) (bool, action.Response) {
// if this is to create bid conversation, everything except bidConvId is needed
// if this is just to add an offer from bidder, only needs bidConvId, bidder(to sign), amount
createBid := CreateBid{}
err := createBid.Unmarshal(tx.Data)
if err != nil {
return helpers.LogAndReturnFalse(ctx.Logger, action.ErrWrongTxType, createBid.Tags(), err)
}
//1. check if this is to create a bid conversation or just add an offer
bidConvId := createBid.BidConvId
if len(createBid.BidConvId) == 0 {
// check asset availability
available, err := IsAssetAvailable(ctx, createBid.AssetName, createBid.AssetType, createBid.AssetOwner)
if err != nil || available == false {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrInvalidAsset, createBid.Tags(), err)
}
bidConvId, err = createBid.createBidConv(ctx)
if err != nil {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrFailedCreateBidConv, createBid.Tags(), err)
}
}
//2. verify bidConvId exists in ACTIVE store
bidMasterStore, err := GetBidMasterStore(ctx)
if err != nil {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrGettingBidMasterStore, createBid.Tags(), err)
}
if !bidMasterStore.BidConv.WithPrefixType(bid_data.BidStateActive).Exists(bidConvId) {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrBidConvNotFound, createBid.Tags(), err)
}
bidConv, err := bidMasterStore.BidConv.WithPrefixType(bid_data.BidStateActive).Get(bidConvId)
if err != nil {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrGettingBidConv, createBid.Tags(), err)
}
//3. check asset availability if this is just to add an offer
if len(createBid.BidConvId) != 0 {
available, err := IsAssetAvailable(ctx, bidConv.AssetName, bidConv.AssetType, bidConv.AssetOwner)
if err != nil || available == false {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrInvalidAsset, createBid.Tags(), err)
}
}
//4. check bidder's identity
if !createBid.Bidder.Equal(bidConv.Bidder) {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrWrongBidder, createBid.Tags(), err)
}
//5. check expiry
deadLine := time.Unix(bidConv.DeadlineUTC, 0)
if deadLine.Before(ctx.Header.Time.UTC()) {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrExpiredBid, createBid.Tags(), err)
}
offerCoin := createBid.Amount.ToCoin(ctx.Currencies)
//6. get the active counter offer
activeCounterOffer, err := bidMasterStore.BidOffer.GetActiveOffer(bidConvId, bid_data.TypeCounterOffer)
// in this case there can be no counter offer if this is the beginning of bid conversation
if err != nil || (len(createBid.BidConvId) != 0 && activeCounterOffer == nil) {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrGettingActiveCounterOffer, createBid.Tags(), err)
}
if activeCounterOffer != nil {
//7. amount needs to be less than active counter offer from owner
activeOfferCoin := activeCounterOffer.Amount.ToCoin(ctx.Currencies)
if activeOfferCoin.LessThanEqualCoin(offerCoin) {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrAmountMoreThanActiveCounterOffer, createBid.Tags(), err)
}
//8. set active counter offer to inactive
err = DeactivateOffer(false, bidConv.Bidder, ctx, activeCounterOffer, bidMasterStore)
if err != nil {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrDeactivateOffer, createBid.Tags(), err)
}
}
//9. lock amount
err = ctx.Balances.MinusFromAddress(createBid.Bidder, offerCoin)
if err != nil {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrLockAmount, createBid.Tags(), err)
}
//10. add new offer to offer store
createBidOffer := bid_data.NewBidOffer(
bidConvId,
bid_data.TypeBidOffer,
ctx.Header.Time.UTC().Unix(),
createBid.Amount,
bid_data.BidAmountLocked,
)
err = bidMasterStore.BidOffer.SetActiveOffer(*createBidOffer)
if err != nil {
return helpers.LogAndReturnFalse(ctx.Logger, bid_data.ErrAddingOffer, createBid.Tags(), err)
}
return helpers.LogAndReturnTrue(ctx.Logger, createBid.Tags(), "create_bid_success")
}
First we deserialize the transaction into CreateBid
struct as done in Validate
method.
🛠 To return error or info in run function, we can utilize
LogAndReturnFalse
andLogAndReturnTrue
functions fromhelpers
package.
And we check from the given parameters after deserialization, if the bid conversation id is not provided, then we need to create a new bid conversation. Before doing this we will also check the availability of the bid asset.
After this we will get our store from context, this part is wrapped in GetBidMasterStore
function in common.go
, you can follow the logic in this function to get your external data store.
func GetBidMasterStore(ctx *action.Context) (*bid_data.BidMasterStore, error) {
store, err := ctx.ExtStores.Get("extBidMaster")
if err != nil {
return nil, bid_data.ErrGettingBidMasterStore.Wrap(err)
}
bidMasterStore, ok := store.(*bid_data.BidMasterStore)
if ok == false {
return nil, bid_data.ErrAssertingBidMasterStore
}
return bidMasterStore, nil
}
After we got the store from context, we need to assert it into our store type.
Store name for external app will start with ext
, this will be elaborated in step 7 later when we register the external app into OneLedger blockchain main application.
Once we have our store, we check if the bid conversation id exists in the ACTIVE store, which means this bid conversation is active.
And we need to check asset availability again if this transaction has bid conversation id in the first place, which means its purpose is to just add another offer into the bid conversation.
Then we need to check the expiry of the bid conversation, in case it is expired.
🛠 We can use
ctx.Header.Time.UTC()
to get the current block time in utc timestamp. This timestamp represents the creation time of a block. This timestamp is synced across the network, be sure to use the UTC version.
After expiry is checked, we need to get active counter offer of this bid conversation. As per design of this example app, there will only be zero to one active offer for one bid conversation at one time, the offer can be bid offer or counter offer.
It makes sense if we cannot get any counter offer right now if bid conversation id is not included in the transaction, which means this is the beginning of the bid conversation.
If there is counter offer then we need to check if the current offer amount is lower then counter offer amount.
🛠 We can use
action.Amount
for OLT amount operations. When we dealing with amount operations, float can cause trouble in terms of inacuracy. So we need to convert the amount to a smallest unit that represent the amount. We can useactiveCounterOffer.Amount.ToCoin(ctx.Currencies)
to converterx
amount of any currency tox * 10^18
coins Then we can compare/calculate the amounts easily.
After this, we deactivate current active counter offer, and lock the amount from bidders address using builtin function ctx.Balances.MinusFromAddress(createBid.Bidder, offerCoin)
. This way we can prevent spam that calls for bidding without corresponding amount of OLT.
And finally we set the new offer to be the active offer.
Block function includes two parts that will run separately at block beginner and block ender.
As mentioned before, block function provides the ability to run transactions automatically based on some conditions.
We can setup conditions based on business logic or just based on timing.
external_apps
├── bid(example project folder)
│ ├── bid_action
│ ├── bid_block_func <---
│ ├── bid_data
│ └── bid_error
├── common(common utility folder)
└── init.go
external_apps
├── bid(example project folder)
│ ├── bid_action
│ ├── bid_block_func
│ │ └── bid_block_func.go <---
│ ├── bid_data
│ └── bid_error
├── common(common utility folder)
└── init.go
Inside this bid_block_func.go
there will be two parts of functions.
Let's use bid app as the example.
Function AddExpireBidTxToQueue
will handle the condition checking part, if any bid conversation has reached the deadline, we will add the expire bid transaction to a queue, those will be excuted later in block ender.
func AddExpireBidTxToQueue(i interface{}) {
// 1. get all the needed stores
extParam, ok := i.(common.ExtParam)
if ok == false {
extParam.Logger.Error("failed to assert extParam in block beginner")
return
}
bidMaster, err := extParam.ActionCtx.ExtStores.Get("extBidMaster")
if err != nil {
extParam.Logger.Error("failed to get bid master store in block beginner", err)
return
}
bidMasterStore, ok := bidMaster.(*bid_data.BidMasterStore)
if ok == false {
extParam.Logger.Error("failed to assert bid master store in block beginner", err)
return
}
bidConvStore := bidMasterStore.BidConv
// 2. iterate all the bid conversations and pick the ones that needs to be expired
bidConvStore.Iterate(func(id bid_data.BidConvId, bidConv *bid_data.BidConv) bool {
// check expiry
deadLine := time.Unix(bidConv.DeadlineUTC, 0)
if deadLine.Before(extParam.Header.Time) {
// get tx
tx, err := GetExpireBidTX(bidConv.BidConvId, extParam.Validator)
if err != nil {
extParam.Logger.Error("Error in building TX of type RequestDeliverTx(expire)", err)
return true
}
// Add tx to expire prefix of transaction store
err = extParam.InternalTxStore.AddCustom("extBidExpire", string(bidConv.BidConvId), &tx)
if err != nil {
extParam.Logger.Error("Error in adding to Expired Queue :", err)
return true
}
// Commit the state
extParam.InternalTxStore.State.Commit()
}
return false
})
}
First we can get all the external stores we need from input, and assert it to common.ExtParam
This struct looks like this:
type ExtParam struct {
InternalTxStore *transactions.TransactionStore
Logger *log.Logger
ActionCtx action.Context
Validator keys.Address
Header abci.Header
Deliver *storage.State
}
🛠
InternalTxStore
is where we save the transaction to be excuted later
Logger
will provide the logging functionality
ActionCtx
will contain all the stores we need
Validator
will be the address that signs the transactions
Header
will provide the current block height and block time, which can be used to check expiry
Deliver
state will be used to directly commit the state, this is only needed in block function transactions
Similar as before, we can get our external strore by extParam.ActionCtx.ExtStores.Get("extBidMaster")
.
After we have our store, we iterate all the bid conversations to look for anyone that has reached deadline. And construct the corresponding expire bid transaction to the using GetExpireBidTX
func GetExpireBidTX(bidConvId bid_data.BidConvId, validatorAddress keys.Address) (abci.RequestDeliverTx, error) {
expireBid := &bid_action.ExpireBid{
BidConvId: bidConvId,
ValidatorAddress: validatorAddress,
}
txData, err := expireBid.Marshal()
if err != nil {
return abci.RequestDeliverTx{}, err
}
internalFinalizeTx := abci.RequestDeliverTx{
Tx: txData,
XXX_NoUnkeyedLiteral: struct{}{},
XXX_unrecognized: nil,
XXX_sizecache: 0,
}
return internalFinalizeTx, nil
}
First we need to create a pointer to the expire bid transaction, and serialize it, and include it into an abci.RequestDeliverTx
object.
After successfully constructed the transaction, we add it into InternalTxStore
with custom prefix extBidExpire
.
Then in the function PopExpireBidTxFromQueue
below, we will pop the transactions and excute them in blockender.
func PopExpireBidTxFromQueue(i interface{}) {
//1. get the internal bid tx store
bidParam, ok := i.(common.ExtParam)
if ok == false {
bidParam.Logger.Error("failed to assert bidParam in block ender")
return
}
//2. get all the pending txs
var expiredBidConvs []abci.RequestDeliverTx
bidParam.InternalTxStore.IterateCustom("extBidExpire", func(key string, tx *abci.RequestDeliverTx) bool {
expiredBidConvs = append(expiredBidConvs, *tx)
return false
})
//3. execute all the txs
for _, bidConv := range expiredBidConvs {
bidParam.Deliver.BeginTxSession()
actionctx := bidParam.ActionCtx
txData := bidConv.Tx
newExpireTx := bid_action.ExpireBidTx{}
newExpire := bid_action.ExpireBid{}
err := newExpire.Unmarshal(txData)
if err != nil {
bidParam.Logger.Error("Unable to UnMarshal TX(Expire) :", txData)
continue
}
uuidNew, _ := uuid.NewUUID()
rawTx := action.RawTx{
Type: bid_action.BID_EXPIRE,
Data: txData,
Fee: action.Fee{},
Memo: uuidNew.String(),
}
ok, _ := newExpireTx.ProcessDeliver(&actionctx, rawTx)
if !ok {
bidParam.Logger.Error("Failed to Expire : ", txData, "Error : ", err)
bidParam.Deliver.DiscardTxSession()
continue
}
bidParam.Deliver.CommitTxSession()
}
//4. clear txs in transaction store
bidParam.InternalTxStore.IterateCustom("extBidExpire", func(key string, tx *abci.RequestDeliverTx) bool {
ok, err := bidParam.InternalTxStore.DeleteCustom("extBidExpire", key)
if !ok {
bidParam.Logger.Error("Failed to clear expired bids queue :", err)
return true
}
return false
})
bidParam.InternalTxStore.State.Commit()
}
We first get the all the pending transactions with our customized prefix extBidExpire
from bidParam.InternalTxStore
.
Then for each pending transaction, we use bidParam.Deliver.BeginTxSession()
to start a transaction session, deserialize the transaction data into transaction object, add a uuid to the memo field to construct the raw transaction.
After this, we directly pass it to ProcessDeliver
, and commit this transaction session if everything is ok, or disgard this session if any error occurs.
Finally we delete all our transactions from InternalTxStore
and commit the state.
RPC pacakge will handle all the query request that pointing to supported query service. We will add some query services in this package.
external_apps
├── bid(example project folder)
│ ├── bid_action
│ ├── bid_block_func
│ ├── bid_data
│ ├── bid_error
│ └── bid_rpc <---
├── common(common utility folder)
└── init.go
external_apps
├── bid(example project folder)
│ ├── bid_action
│ ├── bid_block_func
│ ├── bid_data
│ ├── bid_error
│ └── bid_rpc
│ └── bid_request_types.go <---
├── common(common utility folder)
└── init.go
Inside this file you can define your request and reply types.
external_apps
├── bid(example project folder)
│ ├── bid_action
│ ├── bid_block_func
│ ├── bid_data
│ ├── bid_error
│ └── bid_rpc
│ ├── bid_request_types.go
│ └── errors.go <---
├── common(common utility folder)
└── init.go
Inside this file you can define your rpc service errors as before.
external_apps
├── bid(example project folder)
│ ├── bid_action
│ ├── bid_block_func
│ ├── bid_data
│ ├── bid_error
│ └── bid_rpc
│ ├── bid_request_types.go
│ ├── bid_rpc_query.go <---
│ └── errors.go
├── common(common utility folder)
└── init.go
In this service.go
file, first we need to define our service and its constructor, also Name
function, which will be used in the registeration part in step 7.
type Service struct {
balances *balance.Store
currencies *balance.CurrencySet
ons *ons.DomainStore
logger *log.Logger
bidMaster *bid_data.BidMasterStore
}
func Name() string {
return "bid_query"
}
func NewService(balances *balance.Store, currencies *balance.CurrencySet,
domains *ons.DomainStore, logger *log.Logger, bidMaster *bid_data.BidMasterStore) *Service {
return &Service{
currencies: currencies,
balances: balances,
ons: domains,
logger: logger,
bidMaster: bidMaster,
}
}
The parameters in the Service
struct can be chosen when register the app in step 7.
For example, if your app has nothing to do with OneLedger Naming Service(ONS), you don't need to include ons
as a parameter.
And we will create some services, for example, one is to return a bid conversation by id, the other is to return all the bid conversation that satisfy the query conditions.
func (svc *Service) ShowBidConv(req bid_rpc.ListBidConvRequest, reply *bid_rpc.ListBidConvsReply) error {
bidConv, _, err := svc.bidMaster.BidConv.QueryAllStores(req.BidConvId)
if err != nil {
return bid_rpc.ErrGettingBidConvInQuery.Wrap(err)
}
inactiveOffers := svc.bidMaster.BidOffer.GetInActiveOffers(bidConv.BidConvId, bid_data.TypeInvalid)
activeOffer, err := svc.bidMaster.BidOffer.GetActiveOffer(bidConv.BidConvId, bid_data.TypeInvalid)
if err != nil {
return bid_rpc.ErrGettingActiveOfferInQuery.Wrap(err)
}
activeOfferField := bid_data.BidOffer{}
if activeOffer != nil {
activeOfferField = *activeOffer
}
bcs := bid_rpc.BidConvStat{
BidConv: *bidConv,
ActiveOffer: activeOfferField,
InactiveOffers: inactiveOffers,
}
*reply = bid_rpc.ListBidConvsReply{
BidConvStats: []bid_rpc.BidConvStat{bcs},
Height: svc.bidMaster.BidConv.GetState().Version(),
}
return nil
}
func (svc *Service) ListBidConvs(req bid_rpc.ListBidConvsRequest, reply *bid_rpc.ListBidConvsReply) error {
// Validate parameters
if len(req.Owner) != 0 {
err := req.Owner.Err()
if err != nil {
return bid_rpc.ErrInvalidOwnerAddressInQuery.Wrap(err)
}
}
if len(req.Bidder) != 0 {
err := req.Bidder.Err()
if err != nil {
return bid_rpc.ErrInvalidBidderAddressInQuery.Wrap(err)
}
}
// Query in single store if specified
var bidConvs []bid_data.BidConv
if req.State != bid_data.BidStateInvalid {
bidConvs = svc.bidMaster.BidConv.FilterBidConvs(req.State, req.Owner, req.AssetName, req.AssetType, req.Bidder)
} else { // Query in all stores otherwise
active := svc.bidMaster.BidConv.FilterBidConvs(bid_data.BidStateActive, req.Owner, req.AssetName, req.AssetType, req.Bidder)
succeed := svc.bidMaster.BidConv.FilterBidConvs(bid_data.BidStateSucceed, req.Owner, req.AssetName, req.AssetType, req.Bidder)
rejected := svc.bidMaster.BidConv.FilterBidConvs(bid_data.BidStateRejected, req.Owner, req.AssetName, req.AssetType, req.Bidder)
expired := svc.bidMaster.BidConv.FilterBidConvs(bid_data.BidStateExpired, req.Owner, req.AssetName, req.AssetType, req.Bidder)
cancelled := svc.bidMaster.BidConv.FilterBidConvs(bid_data.BidStateCancelled, req.Owner, req.AssetName, req.AssetType, req.Bidder)
bidConvs = append(bidConvs, active...)
bidConvs = append(bidConvs, succeed...)
bidConvs = append(bidConvs, rejected...)
bidConvs = append(bidConvs, expired...)
bidConvs = append(bidConvs, cancelled...)
}
// Organize reply packet:
// Bid conversations and their offers
bidConvStats := make([]bid_rpc.BidConvStat, len(bidConvs))
for i, bidConv := range bidConvs {
inactiveOffers := svc.bidMaster.BidOffer.GetInActiveOffers(bidConv.BidConvId, bid_data.TypeInvalid)
activeOffer, err := svc.bidMaster.BidOffer.GetActiveOffer(bidConv.BidConvId, bid_data.TypeInvalid)
if err != nil {
return bid_rpc.ErrGettingActiveOfferInQuery.Wrap(err)
}
activeOfferField := bid_data.BidOffer{}
if activeOffer != nil {
activeOfferField = *activeOffer
}
bcs := bid_rpc.BidConvStat{
BidConv: bidConv,
ActiveOffer: activeOfferField,
InactiveOffers: inactiveOffers,
}
bidConvStats[i] = bcs
}
*reply = bid_rpc.ListBidConvsReply{
BidConvStats: bidConvStats,
Height: svc.bidMaster.BidConv.GetState().Version(),
}
return nil
}
In both services above, we use BidConv.GetState().Version()
to get the current height and put it into the reply.
When we call our services using rpc protocol, the method will be bid_query.ShowBidConv
and bid_query.ListBidConvs
At this point, all the functionalities are done, but they are not connected with the main application yet. We need to register all the layers into it.
external_apps
├── bid(example project folder)
│ ├── bid_action
│ ├── bid_block_func
│ ├── bid_data
│ ├── bid_error
│ ├── bid_rpc
│ └── init.go <---
├── common(common utility folder)
└── init.go
Inside this init.go
(under bid
directory) we will add a function called LoadAppData
func LoadAppData(appData *common.ExtAppData) {
logWriter := os.Stdout
logger := log.NewLoggerWithPrefix(logWriter, "extApp").WithLevel(log.Level(4))
//load txs
bidCreate := common.ExtTx{
Tx: bid_action.CreateBidTx{},
Msg: &bid_action.CreateBid{},
}
bidCancel := common.ExtTx{
Tx: bid_action.CancelBidTx{},
Msg: &bid_action.CancelBid{},
}
bidExpire := common.ExtTx{
Tx: bid_action.ExpireBidTx{},
Msg: &bid_action.ExpireBid{},
}
counterOffer := common.ExtTx{
Tx: bid_action.CounterOfferTx{},
Msg: &bid_action.CounterOffer{},
}
bidderDecision := common.ExtTx{
Tx: bid_action.BidderDecisionTx{},
Msg: &bid_action.BidderDecision{},
}
ownerDecision := common.ExtTx{
Tx: bid_action.OwnerDecisionTx{},
Msg: &bid_action.OwnerDecision{},
}
appData.ExtTxs = append(appData.ExtTxs, bidCreate)
appData.ExtTxs = append(appData.ExtTxs, bidCancel)
appData.ExtTxs = append(appData.ExtTxs, bidExpire)
appData.ExtTxs = append(appData.ExtTxs, counterOffer)
appData.ExtTxs = append(appData.ExtTxs, bidderDecision)
appData.ExtTxs = append(appData.ExtTxs, ownerDecision)
//load stores
if dupName, ok := appData.ExtStores["extBidMaster"]; ok {
logger.Errorf("Trying to register external store %s failed, same name already exists", dupName)
return
} else {
appData.ExtStores["extBidMaster"] = bid_data.NewBidMasterStore(appData.ChainState)
}
//load services
balances := balance.NewStore("b", storage.NewState(appData.ChainState))
domains := ons.NewDomainStore("ons", storage.NewState(appData.ChainState))
olt := balance.Currency{Id: 0, Name: "OLT", Chain: chain.ONELEDGER, Decimal: 18, Unit: "nue"}
currencies := balance.NewCurrencySet()
err := currencies.Register(olt)
if err != nil {
logger.Errorf("failed to register currency %s", olt.Name, err)
return
}
appData.ExtServiceMap[bid_rpc_query.Name()] = bid_rpc_query.NewService(balances, currencies, domains, logger, bid_data.NewBidMasterStore(appData.ChainState))
//load beginner and ender functions
err = appData.ExtBlockFuncs.Add(common.BlockBeginner, bid_block_func.AddExpireBidTxToQueue)
if err != nil {
logger.Errorf("failed to load block beginner func", err)
return
}
err = appData.ExtBlockFuncs.Add(common.BlockEnder, bid_block_func.PopExpireBidTxFromQueue)
if err != nil {
logger.Errorf("failed to load block ender func", err)
return
}
}
First we create a log writer and include it into a logger, so that we can use it to log info and errors in the app.
Then we create objects of transactions, and wrap each pair of them into the common.ExtTx
struct. And add them to appData.ExtTxs
.
After this we load all our external stores into appData.ExtStores
map, the key will start with ext
as mentioned before.
Next is to load our rpc services, if your app needs to check balance or ONS ownership in query part, you can pull those stores from chainstate using balances := balance.NewStore("b", storage.NewState(appData.ChainState))
and domains := ons.NewDomainStore("ons", storage.NewState(appData.ChainState))
. Here b
and ons
are fixed prefix for those stores.
And we need to put OLT as our currency in the external app.
Next we will add our block functions into appData.ExtBlockFuncs
, make sure functions for block beginner and block ender are added with correct key (common.BlockBeginner
and common.BlockEnder
)
And finally in the init.go
of external_apps
, we need to add one line into the init
function, common.Handlers.Register(bid.LoadAppData)
. This way the registeration part is finished.
© OneLedger 2018-2020 Contact Information