In this tutorial, we will explore what a pay-as-you-go uploading service using IOTA for payments could look like. While IOTA was designed to enable the Internet-of-Things, its lack of fees also makes it great for micropayments, allowing it to be used for on-demand/pay-as-you-go services.
The source code for this tutorial and the instructions on how to run it can be found at https://github.com/bmavity/iota-upload-poc.
The tutorial makes a few assumptions
- You are already familiar with the basics of using the IOTA JavaScript API to generate addresses and make payments. If not, please review the introductory level tutorial, and come back afterward to continue.
- You are either familiar with, or can follow JavaScript that uses some common ES6 syntax (arrow functions, classes, const/let). See this overview for more information.
- You are either running your own IOTA node (see the setup instructions) or can connect to the development sandbox (as of writing, the sandbox is not available, but the default configuration will be updated when it is.)
- You have http://nodejs.org installed
- One wallet seed to send payments, which must have a balance
- A second wallet seed to receive payments
- Change the IOTA connection settings in the iota.wallet.config.js file to match your local environment.
The project is divided into client and server components, but we will focus only on the client, as it has all of the logic. In places, the client uses ES6 syntax, and the UI is developed using React. It is not necessary to be familiar with either, as the application logic is encapsulated from the UI. Those curious about the server code can review it directly, as it is short and commented.
The project as a whole is structured in a somewhat standard way for React.js applications, described in the following links. These links are not necessary to follow the tutorial, but may be interesting reading for some.
- Libraries and Dependency Setup
- Folder Structure
In this section, we’ll discuss the functionality we require and demonstrate how it is performed. There are two actors in this example, and both are shown on the same page. First, we will be simulating a company which is selling the upload service, and secondly, we will simulate a customer using that service.
The company is providing the upload service. To do this, the company will require a seed for the company wallet. Ideally, the company payment address would be provided to the client from the server, but in our case, it is easier to allow entry of the company seed in the UI. To do so, open the panel on the right hand side of the screen and enter in a seed. Once the seed is entered, the wallet data will be loaded and a payment address will be generated. This will be populated in the main section of the application.
To use the upload service, the customer must provide a seed to a wallet that has a balance. Once the balance is verified, the upload portion of the application becomes active. The upload component is provided by http://uppy.io which has a slick interface allowing for multiple resumable uploads. Once the customer starts to upload the files, the software will monitor the upload progress, and after the threshold of 1 mb has been reached, it will pause the upload and create a transaction with a payment based on the number of bytes uploaded. Once the transaction is successful, the upload resumes until the 1mb threshold has been met again. When the file is fully uploaded, one final payment is made.
Now that we’ve had an overview of the application features, let’s take a look at some of the code. We will be focusing on the following files
- src/client/modules/wallet/wallet.js
- src/client/modules/upload/PaidUpload.js
- src/client/modules/upload/uploader.js
These files will be using APIs from iota.lib.js and Uppy. Feel free to review the docs for each if you need any further information. You will also likely notice the stateUpdater object in the following code samples. It is used to update the UI state.
When a Company seed is entered and the Set Seed button is clicked,the setCompanySeed function is called, which updates the UI and makes a standard call to the iota.lib.js API to generate the address. This should be familiar to anyone who has read the leaderboard example. After the address is generated, the UI is updated again.
export function setCompanySeed(seed) {
// Update UI with Company seed
stateUpdater.setCompanySeed(seed)
// Deterministically generates a new address
// with checksum for the specified seed
iota.api.getNewAddress(seed, { checksum: true }, (err, address) => {
if (err) {
// If there was an error generating an address
// reset the seed to allow for reentry
stateUpdater.setCompanySeed(null)
} else {
// Update the UI with the Company address
stateUpdater.setCompanyAddress(address)
}
})
}
To verify the Customer balance, the Customer will need to enter a seed. Once the seed is entered, the UI is updated with seed, the account information is retrieved from the API, and the Customer balance is set in the UI.
export function setCustomerSeed(seed) {
// Update UI with Customer seed
stateUpdater.setCustomerSeed(seed)
// Gets account information for the specified seed
iota.api.getAccountData(seed, (err, accountData) => {
if (err) {
// If there was an error getting the account data
// reset the seed to allow for reentry
stateUpdater.setCustomerSeed(null)
} else {
// Update the UI with the Customer balance
stateUpdater.setCustomerBalance(accountData.balance)
}
})
}
To upload files, we are using the Uppy library. Currently, it is a work in progress, so they reserve the right to change the API at any time, and they also caution that it should not be used in a production environment. But, it will do just fine for our purposes. Uppy is designed to be completely modular, with all logic residing in plugins that are added to a coordinator object with an API that consists of two setup methods and status events. The documentation is not great at this time, so if you are interested in learning more, prepare to do some digging through the code.
The desired behavior of the application is to allow the Customer to add files to the upload, but not to start the uploads automatically so that the Customer can decide when to start being charged. It also will keep track of each individual file that is being uploaded so that the progress and payments for each individual file can be identified. When the application starts, the following code is run to configure Uppy.
// Initialize Uppy with autoProceed: false so that uploads
// do not start automaticlly.
const uppy = Uppy({ autoProceed: false, debug: true })
// Create a PaidUpload instance for each file that is uploaded
uppy.on('core:upload-started', (fileId, upload) => {
// Encapsulate the Uppy fileId and upload into an object
// that will keep track of progress and payments
uploaders[fileId] = new PaidUpload(fileId, upload)
})
// When a file is finished uploading, complete the PaidUpload
uppy.on('core:upload-success', (fileId, url) => {
// Call complete method on matching file object
uploaders[fileId].complete(url)
})
The Uppy UI (DashboardUI) we are using adds itself to the page when Uppy is initialized. Due to the use of React, the intialization code needs to be separated so that it can run when the DashboardUI is first displayed. The initilization code should ensure that the application has a nice UI, pausable/resumable uploads, and that it can get files from multiple places.
// Called once to initialize the Uppy UI
export function initalizeUploader(config) {
// UI plugin for friendly display
uppy.use(Dashboard, config)
// Allows webcam video to be uploaded
.use(Webcam, { target: Dashboard })
// Displays information messages
.use(Informer, { target: Dashboard })
// File uploading protocol that allows uploads to be paused/resumed
.use(Tus10, { endpoint: `//localhost:${WEB_PORT}/files` })
// Display the configured Uppy UI
uppy.run()
}
Now that we have the UI initialized, we can add files to the Uppy Dashboard. Since we have specified that the uploads should not start automatically, we will need to click the upload button to begin. Once clicked, the upload will start and the core:upload-started event will fire, causing a new PaidUpload object to be created. The PaidUpload constructor adds an onProgress
handler to the upload
object to determine when a payment needs to be made. Unfortunately, Uppy does not expose this functionality on a per-file basis, so we have to make sure to call Uppy's original onProgress
handler before continuing with the code that we need to execute.
constructor(fileId, upload) {
...
// Initialize all byte fields to 0
// assumes upload has not been resumed
this.bytesPaid = 0
this.bytesPendingPayment = 0
this.bytesUploaded = 0
// Save original onProgress handler
const originalOnProgress = upload.options.onProgress
// eslint-disable-next-line no-param-reassign
upload.options.onProgress = (bytesUploaded, bytesTotal) => {
// Invoke original onProgress handler to avoid
// possible breakage of Uppy functionality
originalOnProgress(bytesUploaded, bytesTotal)
// Process the uploaded bytes
this.addBytesToUpload(bytesUploaded)
}
}
While the file is uploading, the onProgress
handler we added in the PaidUpload
constructor will be called periodically with the cumulative bytes that have been uploaded so far, as well as the total size of the file in bytes. We will need to determine whether the upload should continue or whether it should be paused and a payment should be made. In our app, we are going to charge users 1 IOTA for every MB uploaded. We will keep track of the number of bytes that have been paid with confirmed transactions, as well as the number of bytes that have pending payments. (Theoretically, uploads should only continue once a payment is confirmed, but due to the way the Uppy Dashboard works, uploads can be resumed before a payment is processed.)
Once the number of unpaid bytes is calculated, we'll make a payment. After the payment is made, the status will be returned in the callback. It will be one of error, pending, or confirmed. The payment status will be updated and if there was not an error in processing the payment, the upload will resume.
addBytesToUpload(bytesUploaded) {
// Determine how many bytes need to be paid
const unpaidBytes = this.getUnpaidBytes(bytesUploaded)
// If there are any bytes that have not been paid,
// pause the upload and make a new payment
if (unpaidBytes > 0) {
this.pauseUpload()
this.makePayment(unpaidBytes)
}
}
getUnpaidBytes(bytesUploaded) {
// To avoid double charging Customers for uploaded bytes
// include the bytes that already have a transaction
// created or are "pending" confirm
const totalPendingBytes = this.bytesPaid + this.bytesPendingPayment
return bytesUploaded - totalPendingBytes
}
makePayment(unpaidBytes) {
// Calculate the amount of IOTA due, rounding up.
const paymentAmount = Math.ceil(parseInt(unpaidBytes, 10) / bytesForOneIota)
// Generate payment id
const paymentId = this.getNextPaymentId()
// Add the payment as processing
this.payments[paymentId] = {
bytesPaid: paymentAmount * bytesForOneIota,
paymentId,
paymentAmount,
status: 'processing',
}
// Update the file data to reflect the payment
this.updateFileData()
// Call wallet API to complete the payment
makeWalletPayment(this.fileId, paymentId, paymentAmount, (status) => {
// Update payment status
this.payments[paymentId].status = status
// Update Paid Upload byte totals
this.updateFileData()
// If the payment attempt did not result in an error,
// resume the upload
if (status !== 'error') {
this.resumeUpload()
}
})
}
Once the upload is complete, the core:upload-success handler will be called, which will then complete the upload.
complete(url) {
// store the download url for the file
this.url = url
// Determine how many bytes left that need to be paid
const unpaidBytes = this.getUnpaidBytes(this.bytesTotal)
// If there are any bytes that have not been paid,
// make a final payment
if (unpaidBytes > 0) {
this.makePayment(unpaidBytes)
}
}
- IOTA addresses are secure for multiple payments as long as nothing is spent from that address. To ensure that a company can spend in the middle of an upload:
- generate a new address for each payment
- add the file id as a message to the transaction so that transactions can be rolled up into a full payment.
- Currently, if the page is refreshed when an upload is paused, and the Customer resumes the upload, the application will charge the Customer for the full size of the file, instead of calculating previous payments. Use the file id added to the transactions in the previous exercise to calculate existing payments to ensure the Customer is charged fairly.
- Uppy has additional plugins for Dropbox and Google Drive. Enable the file uploader to get files from both.
While the proof-of-concept described here has a few rough edges, it is sufficient enough to display that IOTA can be effectively be used for micropayments.