Skip to content

External App Tutorial

hao edited this page Sep 18, 2020 · 37 revisions

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.

Introduction

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.

  1. Pre Start
  2. Create your external app folder
  3. Create error codes package
  4. Create data package
  5. Create action package
  6. Create block function package
  7. Create RPC package
  8. Register the app into OneLedger blockchain

0. Pre-Start

System Requirements

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

Configurations

Setup Environment Variables

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

Install ClevelDB

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

Clone the repo

cd $OLPATH
git clone github.com/Oneledger/protocol

Install the required dependencies

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.

Choose your IDE

You can use Goland or VScode. For Goland, it comes with a 30-day trial.

Steps to create an external app

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.

1. Create your external app folder

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.

2. Create error codes package

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
	...
)

3. Create data package

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.

Create data folder as below

external_apps
├── bid(example project folder)  
│   ├── bid_data <---
│   └── bid_error
├── common(common utility folder)
└── init.go

Define basic data types that will be used in the external app

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
)

Define data layer errors

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.

Define constants and other components in init.go 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.

Define structs that will be used in the external app

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.

⚠️ Remember to use proper json tag so that it can be successfully serialized and deserialized. The naming convention we use for json tag is camel style.

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. The String() 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.

Define data stores that will be used in the external app

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 this defer func() { bcs.prefix = prefix }() to revert the change we made to the store. Because WithPrefixType 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.

Create unit test for data stores

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.

4. Create action package

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.

Create action folder as below

external_apps
├── bid(example project folder)  
│   ├── bid_action <---
│   ├── bid_data
│   └── bid_error
├── common(common utility folder)
└── init.go

Create initialization file in action package

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.

Create a transaction

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 the Validate 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 and LogAndReturnTrue functions from helpers 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 use activeCounterOffer.Amount.ToCoin(ctx.Currencies) to converter x amount of any currency to x * 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.

5. Create block function package

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.

Create block function folder

external_apps
├── bid(example project folder)  
│   ├── bid_action
│   ├── bid_block_func <---
│   ├── bid_data
│   └── bid_error
├── common(common utility folder)
└── init.go

Create block function file

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.

6. Create RPC package

RPC pacakge will handle all the query request that pointing to supported query service. We will add some query services in this package.

Create RPC query folder

external_apps
├── bid(example project folder)  
│   ├── bid_action
│   ├── bid_block_func
│   ├── bid_data
│   ├── bid_error
│   └── bid_rpc <---
├── common(common utility folder)
└── init.go

Define your request types

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.

Define your rpc service errors

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.

Create RPC query file

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

7. Register the app into OneLedger blockchain

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.

Create init.go file in our external app folder

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.

Clone this wiki locally