Lisk is joining the Optimism Superchain ecosystem in 2024! Learn more about our planned migration to Ethereum, and what it means for our community, here

Building the Lisk Ride Project

LiskRide is a community platform that connects drivers with free seats to passengers looking for a ride in the same direction. We aim to remove intermediaries and allow direct transactions. This project is part of the sharing economy and takes advantage of the Lisk blockchain application platform, and has been developed as a progressive web app based on Reactjs.

By Jean-Michel Alandou

16 Dec 2020

financial-update-november-2020-OG@2x.png

How does it work?

In our ecosystem there are two types of participants:

  • The driver
  • The passenger

Firstly, the prerequisites for the driver are explained, followed by the passenger.

Common passenger and driver prerequisites

Authentication

rsz_capture_d’écran_2020-12-15_à_125837.png
rsz_capture_d’écran_2020-12-15_à_130532.png

To use the LiskRide app, the driver and passenger will need to create their accounts via the authentication page and keep the passphrase secure. The passphrase and address are automatically generated on the "Register" page.

If users already have an account, they will need to log in through the Login page.

Transfer

rsz_capture_d’écran_2020-12-15_à_113933.png

Once logged in, on the Transfer page, users will be able to collect tokens to use the platform. It should be noted that this transfer is made for the POC.

File name
1
2## The driver requirements prerequisites
3
4The following steps are required by the driver:
5
6**Publication of the travel plan**
7
8<Image>
9![ezgif.com-resize 5_0.gif](https://media.lisk.com/ezgif_com_resize_5_0_68545e1f37.gif?width=300&height=531)
10</Image>
11 
12
13The driver can add their travel plan by choosing the city of destination, the city of departure, the date of departure, the fee per seat, and the number of available seats.
html applyAsset(store) { const errors = \[\]; const travel = store.account.getOrDefault(this.asset.travelId); const driver = store.account.get(this.senderId) const driverTravels = driver.asset.driverTravels || \[\] if (!travel.asset.pickUpDate) { const driverTravel = { carId:this.asset.carId, travelId:this.asset.travelId, senderId: this.senderId, driverAdress: this.asset.driverAdress, pickUpLocation: this.asset.pickUpLocation, pickUpDate:this.asset.pickUpDate, availableSeatCount: this.asset.availableSeatCount, pricePerSeat:this.asset.pricePerSeat, destination:this.asset.destination } driverTravels.push(driverTravel) const updatedTravelAccount = { ...travel, asset: { carId:this.asset.carId, travelId:this.asset.travelId, senderId: this.senderId, driverAdress: this.asset.driverAdress, pickUpLocation: this.asset.pickUpLocation, pickUpDate:this.asset.pickUpDate, availableSeatCount: this.asset.availableSeatCount, pricePerSeat:this.asset.pricePerSeat, destination:this.asset.destination } }; const updatedDriverAccount = { ...driver, asset: { ...driver.asset, driverTravels:driverTravels, } }; store.account.set(travel.address, updatedTravelAccount); store.account.set(this.senderId, updatedDriverAccount); }else { errors.push( new TransactionError( 'travel has already been registered', producer.asset.name ) ); } return errors; }
File name
1
2**Reservation of passengers and securing of funds**
3
4<Image>
5![rsz_2unknown.png](https://media.lisk.com/rsz_2unknown_d941f35cfd.png?width=300&height=534)
6</Image>
7 
8
9When a passenger books the journey, the contact information is sent to the driver. In addition, funds are withdrawn from the passenger and deposited in the travel account to guarantee the passenger is able to pay for the journey.
html applyAsset(store) { const errors = \[\]; const travel = store.account.get(this.asset.travelId); const passenger = store.account.get(this.asset.passengerId); const driver = store.account.get(travel.asset.carId); const passengerTravels = passenger.asset.passengerTravels || \[\] const foundDriverTravelIndex = driver.asset.driverTravels.findIndex(element =

element.travelId = this.asset.travelId); const amountTravel = new utils.BigNum(travel.asset.pricePerSeat).mul( new utils.BigNum(this.asset.seatCount) ); const newTravelBalance = new utils.BigNum(travel.balance).add( new utils.BigNum(amountTravel) ); const newPassengerBalance = new utils.BigNum(passenger.balance).sub( newTravelBalance ); if ( !utils.BigNum(passenger.balance).gt("0") || !utils.BigNum(passenger.balance).gte(newTravelBalance) ) { errors.push( new TransactionError( "not enough amount for this travel", this.asset.travelId ) ); } if ( !utils.BigNum(travel.asset.availableSeatCount).gte(this.asset.seatCount) ) { errors.push( new TransactionError( "not enough seat for this travel", this.asset.travelId ) ); } if ( passenger.address driver.address ) { errors.push( new TransactionError( "Driver cannot book seat on his car", this.asset.travelId ) ); } if (errors.length <= 0) { const travelPassengerBalances = travel.asset.travelPassengerBalances || [] const foundTravelPassengerBalanceIndex = travelPassengerBalances.findIndex(element => element.passengerAddress === passenger.address); const foundTravelPassengerBalance = travelPassengerBalances[foundTravelPassengerBalanceIndex]; if(!foundTravelPassengerBalance){ travelPassengerBalances.push({passengerAddress:passenger.address, seatCount:this.asset.seatCount, amountTravel:amountTravel.toString()}) }else{ travelPassengerBalances[foundTravelPassengerBalanceIndex] = {...foundTravelPassengerBalance, seatCount:utils.BigNum(foundTravelPassengerBalance.seatCount).add(this.asset.seatCount).toString(), amountTravel:utils.BigNum(foundTravelPassengerBalance.amountTravel).add(new utils.BigNum(amountTravel)).toString()} } const restSeatCount = new utils.BigNum( travel.asset.availableSeatCount ).sub(this.asset.seatCount); const updatedTravel = { ...travel, asset: { ...travel.asset, travelPassengerBalances:travelPassengerBalances, availableSeatCount: restSeatCount.toString(), }, balance: newTravelBalance.toString(), }; store.account.set(travel.address, updatedTravel); passengerTravels.push(travel) const updatedPassenger = { ...passenger, asset: { ...passenger.asset, passengerTravels:passengerTravels, }, balance: newPassengerBalance.toString(), }; store.account.set(passenger.address, updatedPassenger); driver.asset.driverTravels[foundDriverTravelIndex] = updatedTravel.asset const updatedDriver = { ...driver, asset: { ...driver.asset, driverTravels:driver.asset.driverTravels, } }; store.account.set(driver.address, updatedDriver); } return errors; } ```

Travel

ezgif.com-resize 3_1.gif

When the driver meets the passenger, they must then agree to start the journey together. This action must be done in the presence of both parties. This will unblock the transfer of funds to the driver’s account.

File name
1
2**Receipt of payment**
3
4<Image>
5![ezgif.com-resize 1_0.gif](https://media.lisk.com/ezgif_com_resize_1_0_32639d30ee.gif?width=300&height=535)
6</Image>
7 
8
9The driver will be able to recover the funds and evaluate the passenger via a withdrawal interface accessible through his trip list. In the future, the withdrawal will be possible after a period of 24 hours.
html applyAsset(store) { const errors = \[\]; const travel = store.account.get(this.asset.travelId); const passenger = store.account.get(this.asset.passengerId); const driver = store.account.get(this.asset.carId); const travelDriverBalance = travel.asset.travelDriverBalance || \[\]; const foundTravelDriverBalanceIndex = travelDriverBalance.findIndex( (element) =

element.passengerAddress === this.asset.passengerId ); if ( !travelDriverBalance[foundTravelDriverBalanceIndex].rating && new utils.BigNum( travelDriverBalance[foundTravelDriverBalanceIndex].amountTravel ).gt(0) ) { const amountToWidthdraw = new utils.BigNum( travelDriverBalance[foundTravelDriverBalanceIndex].amountTravel ); travelDriverBalance[foundTravelDriverBalanceIndex] = { ...travelDriverBalance[foundTravelDriverBalanceIndex], rating: this.asset.rating, amountTravel: "0", }; const newTravelBalance = new utils.BigNum(travel.balance).sub( new utils.BigNum(amountToWidthdraw) ); const newDriverBalance = new utils.BigNum(driver.balance).add( amountToWidthdraw ); const ratings = passenger.asset.ratings || []; ratings.push({ rating: this.asset.rating, notedBy: this.senderId, timestamp: this.timestamp, }); const updatedTravelAccount = { ...travel, balance: newTravelBalance.toString(), asset: { ...travel.asset, travelDriverBalance: travelDriverBalance, }, }; const updatedPassengerAccount = { ...passenger, asset: { ...passenger.asset, ratings: ratings, }, }; const updatedDriverAccount = { ...driver, balance: newDriverBalance.toString(), }; store.account.set(travel.address, updatedTravelAccount); store.account.set(passenger.address, updatedPassengerAccount); store.account.set(driver.address, updatedDriverAccount); } return errors; } ```

The passenger requirements prerequisites

The following steps are required by the passenger:

Finding a trip to the desired destination

ezgif.com-resize 2_0.gif

To search for a planned travel journey, the passenger simply has to enter their destination, their city of departure, and the corresponding date. It will then be possible to choose a route from the results obtained. If more information is required, they can contact the driver before booking.

The search was made possible by the use of the extended API by Moosty: @moosty/lisk-extended-api.

File name
1
2**Payment and securing of funds**
3
4<Image>
5![ezgif.com-resize_1.gif](https://media.lisk.com/ezgif_com_resize_1_0d1b346ffa.gif?width=300&height=535)
6</Image>
7 
8
9The passenger books their trip and will be automatically debited for the corresponding amount. These funds will be deposited in the travel account.
html applyAsset(store) { const errors = \[\]; const travel = store.account.get(this.asset.travelId); const passenger = store.account.get(this.asset.passengerId); const driver = store.account.get(travel.asset.carId); const passengerTravels = passenger.asset.passengerTravels || \[\] const foundDriverTravelIndex = driver.asset.driverTravels.findIndex(element =

element.travelId = this.asset.travelId); const amountTravel = new utils.BigNum(travel.asset.pricePerSeat).mul( new utils.BigNum(this.asset.seatCount) ); const newTravelBalance = new utils.BigNum(travel.balance).add( new utils.BigNum(amountTravel) ); const newPassengerBalance = new utils.BigNum(passenger.balance).sub( newTravelBalance ); if ( !utils.BigNum(passenger.balance).gt("0") || !utils.BigNum(passenger.balance).gte(newTravelBalance) ) { errors.push( new TransactionError( "not enough amount for this travel", this.asset.travelId ) ); } if ( !utils.BigNum(travel.asset.availableSeatCount).gte(this.asset.seatCount) ) { errors.push( new TransactionError( "not enough seat for this travel", this.asset.travelId ) ); } if ( passenger.address driver.address ) { errors.push( new TransactionError( "Driver cannot book seat on his car", this.asset.travelId ) ); } if (errors.length <= 0) { const travelPassengerBalances = travel.asset.travelPassengerBalances || [] const foundTravelPassengerBalanceIndex = travelPassengerBalances.findIndex(element => element.passengerAddress === passenger.address); const foundTravelPassengerBalance = travelPassengerBalances[foundTravelPassengerBalanceIndex]; if(!foundTravelPassengerBalance){ travelPassengerBalances.push({passengerAddress:passenger.address, seatCount:this.asset.seatCount, amountTravel:amountTravel.toString()}) }else{ travelPassengerBalances[foundTravelPassengerBalanceIndex] = {...foundTravelPassengerBalance, seatCount:utils.BigNum(foundTravelPassengerBalance.seatCount).add(this.asset.seatCount).toString(), amountTravel:utils.BigNum(foundTravelPassengerBalance.amountTravel).add(new utils.BigNum(amountTravel)).toString()} } const restSeatCount = new utils.BigNum( travel.asset.availableSeatCount ).sub(this.asset.seatCount); const updatedTravel = { ...travel, asset: { ...travel.asset, travelPassengerBalances:travelPassengerBalances, availableSeatCount: restSeatCount.toString(), }, balance: newTravelBalance.toString(), }; store.account.set(travel.address, updatedTravel); passengerTravels.push(travel) const updatedPassenger = { ...passenger, asset: { ...passenger.asset, passengerTravels:passengerTravels, }, balance: newPassengerBalance.toString(), }; store.account.set(passenger.address, updatedPassenger); driver.asset.driverTravels[foundDriverTravelIndex] = updatedTravel.asset const updatedDriver = { ...driver, asset: { ...driver.asset, driverTravels:driver.asset.driverTravels, } }; store.account.set(driver.address, updatedDriver); } return errors; } ```

Travel

ezgif.com-resize 3_2.gif

At the request of the driver, the passenger must agree to begin the journey. This action must be done in the presence of both participants. The transfer of funds to the driver's account can then be made.

File name
1html const { destination, pickUpLocation, pickUpDate, availableSeatCount, } = this.state; const travels = \[\]; this.setState({isLoading:true}) let destinationP = fetch( \`http://localhost:2020/extended-api/accounts?asset=destination&contains=${destination}\` ); let pickUpLocationP = fetch( \`http://localhost:2020/extended-api/accounts?asset=pickUpLocation&contains=${pickUpLocation}\` ); let pickUpDateP = fetch( \`http://localhost:2020/extended-api/accounts?asset=pickUpDate&contains=${pickUpDate}\` ); var search = { availableSeatCount, pickUpDate, pickUpLocation, destination }; Promise.all(\[ destinationP, pickUpLocationP, pickUpDateP, \]) .then((values) => { let promises = \[\]; values.forEach((value) => { promises.push(value.json()); }); Promise.all(promises).then((values) => { values.forEach((value) => { console.log(value); value.data.forEach((v) => travels.push({travelId:v.id, carId:v.carId,...v.asset})); }); const filter = {pickUpDate, pickUpLocation, destination} let results = travels.filter(function(item) { for (var key in filter) { if (item\[key\] === undefined || item\[key\] !== filter\[key\]) return false; } return true; }); this.setState({isLoading:false}) let uniquResult = \_.uniqBy(results, 'travelId'); this.props.updateTravels({travels:uniquResult, search}) this.props.history.push("/home/results"); }); }) .catch((reason) => { this.setState({isLoading:false}) console.log(reason); }); };

Custom Transactions

LiskRide uses the following custom transactions:

TypeNameDescription
30AddAccountInfoTransactionAdd complementary information to the driver’s account
31RegisterTravelTransactionCreate a travel journey as an account
32BookTravelTransactionAdd the passenger to the travel account and their corresponding funds
33StartTravelTransactionAllow the driver to rate the passenger and withdraw the funds
34EndTravelTransactionTransfer funds to the driver's account, rate the passenger, and remove the passenger from the travel account

Conclusion

As a JavaScript developer, it was a pleasant experience to work with the Lisk SDK. Indeed, the use of JavaScript is easily interfaced with other web technologies, especially with my front-end in Reactjs.

I did not encounter any particular difficulty with both the debug and the deployment of the application, which was very advantageous.

I also appreciated the custom transactions which allowed me to focus on the business side of my project.

Now that my proof of concept is developed, the next steps are as follows:

  • Share with the community
  • Improve the business logic
  • Improve the user interface to take into account UX experiences related to the Lisk blockchain
  • Improve the travel search system
  • Add a dispute management system
  • Build a team

In the near future, LiskRide could be an alternative to existing carpooling applications.

Resources

Desktop Web Demo: https://boring-banach-a0e04c.netlify.app

Mobile Web Demo: http://lisk-ride.com

Source Code: https://github.com/blackjmxx/LiskRideApp

Disclaimer: This blog post was written by our community member, Blackjmxx (LinkedIn profile) as part of his participation in the Lisk Builders program.