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.
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
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
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.
1html handleTransfert = (event) => { this.setState({isLoading:true}) let user = JSON.parse(getUser()); try { const fundTransaction = new TransferTransaction({ asset: { recipientId: user.address, amount: utils.convertLSKToBeddows("200"), }, networkIdentifier: networkIdentifier, }); fundTransaction.sign("XXXX XXXX XXXX XXXX"); api.transactions.broadcast(fundTransaction.toJSON()).then(response => { this.setState({isLoading:false}) }).catch(err => { this.setState({isLoading:false}) console.log(JSON.stringify(err.errors, null, 2)); }); } catch (error) { this.setState({isLoading:false}) console.log(error) } };
The driver requirements prerequisites
The following steps are required by the driver:
Publication of the travel plan
The 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.
1html 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; }
Reservation of passengers and securing of funds
When 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.
1html 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
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.
1html applyAsset(store) { const errors = \[\]; const travel = store.account.get(this.asset.travelId); const passenger = store.account.get(this.asset.passengerId); const travelDriverBalance = travel.asset.travelDriverBalance || \[\] const travelPassengerBalances = travel.asset.travelPassengerBalances || \[\] const foundTravelPassengerBalance = travelPassengerBalances.find(element => element.passengerAddress === passenger.address); const foundTravelDriverBalance = travelDriverBalance.find(element => element.passengerAddress === passenger.address); if(!foundTravelDriverBalance){ if(foundTravelPassengerBalance){ travelDriverBalance.push(foundTravelPassengerBalance) } }else{ errors.push( new TransactionError( 'travelDriverBalance has already been setted for driver', this.asset.travelId ) ); } const updatedTravelAccount = { ...travel, asset: { ...travel.asset, travelDriverBalance:travelDriverBalance, } }; if(errors.length === 0){ store.account.set(travel.address, updatedTravelAccount); } return errors; }
Receipt of payment
The 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.
1html 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
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.
1html state = { departure: undefined, pickUpLocation: undefined, pickUpDate: new Date(), availableSeatCount: 0, pricePerSeat: 0, travels:\[\], showCalendarModal: false, isLoading:false }; handleForm = () => { 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); });
Payment and securing of funds
The passenger books their trip and will be automatically debited for the corresponding amount. These funds will be deposited in the travel account.
1html 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
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.
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:
Type | Name | Description |
---|---|---|
30 | AddAccountInfoTransaction | Add complementary information to the driver’s account |
31 | RegisterTravelTransaction | Create a travel journey as an account |
32 | BookTravelTransaction | Add the passenger to the travel account and their corresponding funds |
33 | StartTravelTransaction | Allow the driver to rate the passenger and withdraw the funds |
34 | EndTravelTransaction | Transfer 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.