The OmiseGO iOS SDK allows developers to easily interact with the OmiseGO eWallet.
- Requirements
- Installation
- Usage
- Tests
- Dependencies
- Contributing
- License
- iOS 10.0+
- Xcode 9+
- Swift 4.1
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
gem install cocoapods
To integrate the OmiseGO
SDK into your Xcode project using CocoaPods, add the following line in your Podfile
:
pod 'OmiseGO'
Alternatively you can also specify a version ([read more about the Podfile] (https://guides.cocoapods.org/using/the-podfile.html)):
pod 'OmiseGO', '~> 0.10'
Then, run the following command:
$ pod install
Carthage is a simple, decentralized dependency manager for Cocoa.
brew install carthage
To integrate the omisego
SDK into your Xcode project using Carthage, add the following line in your Cartfile
:
github "omisego/ios-sdk"
Then, run the following command:
carthage update --platform ios
Drag the built OmiseGO.framework
into your Xcode project.
Different part of the SDK work with 2 different protocols: http(s) and ws(s).
This section describes the use of the http client in order to retrieve or create resources.
Before using the SDK to retrieve a resource, you need to initialize an HTTPClient
with a ClientConfiguration
object.
You should do this as soon as you obtain a valid authentication token corresponding to the current user from the Wallet API.
let configuration = ClientConfiguration(baseURL: "https://your.base.url/api",
apiKey: "apiKey",
authenticationToken: "authenticationToken",
debugLog: false)
let client = HTTPClient(config: configuration)
Where:
baseURL
is the URL of the OmiseGO Wallet API, this needs to be an http(s) url.apiKey
is the API key (typically generated on the admin panel)authenticationToken
is the token corresponding to an OmiseGO Wallet user retrievable using one of our server-side SDKs.
You can find more info on how to retrieve this token in the OmiseGO server SDK documentations.
debugLog
is a boolean indicating if the SDK should print logs in the console.
Once you have an initialized client, you can retrieve different resources.
Each call take a Callback
closure that returns a Response
enum:
public enum Response<Data> {
case success(data: Data)
case fail(error: OMGError)
}
You can then use a switch-case to access the data
if the call succeeded or error
if the call failed.
The OMGError
represents an error that have occurred before, during or after the request. It's an enum with 5 cases:
case unexpected(message: String)
case configuration(message: String)
case api(apiError: APIError)
case socketError(message: String)
case decoding(underlyingError: DecodingError)
case other(error: Error)
An error returned by the OmiseGO Wallet server will be mapped to an APIError
which contains informations about the failure.
There are some errors that you really want to handle, especially the ones related to authentication failure. This may occur if the
authenticationToken
is invalid or expired, you can check this using theisAuthorizationError()
method onAPIError
. If theauthenticationToken
is invalid, you should query a new one and setup the client again.
User.getCurrent(using: client) { (userResult) in
switch userResult {
case .success(data: let user):
//TODO: Do something with the user
case .fail(error: let error):
//TODO: Handle the error
}
}
Wallet.getAll(using: client) { (walletsResult) in
switch walletsResult {
case .success(data: let wallets):
//TODO: Do something with the wallets
case .fail(error: let error):
//TODO: Handle the error
}
}
Note: For now a user will have only one wallet so for the sake of simplicity you can get this wallet using:
Wallet.getMain(using: client) { (walletResult) in
switch walletResult {
case .success(data: let wallet):
//TODO: Do something with the wallet
case .fail(error: let error):
//TODO: Handle the error
}
}
Setting.get(using: client) { (settingResult) in
switch settingResult {
case .success(data: let settings):
//TODO: Do something with the settings
case .fail(error: let error):
//TODO: Handle the error
}
}
This returns a paginated filtered list of transactions.
In order to get this list you will need to create a TransactionListParams
object:
let params = TransactionListParams(paginationParams: paginationParams, address: nil)
Where
address
is an optional address that belongs to the current user (primary wallet address by default)paginationParams
is aPaginationParams<Transaction>
object:
let paginationParams = PaginationParams<Transaction>(
page: 1,
perPage: 10,
searchTerm: nil,
searchTerms: nil,
sortBy: .createdAt,
sortDirection: .descending
)
Where:
page
is the page you wish to receive.perPage
is the number of results per page.sortBy
is the sorting field. Available values:.id
,.status
,.from
,.to
,.createdAt
sortDir
is the sorting direction. Available values:.ascending
,.descending
searchTerm
is a term to search for in ALL of the searchable fields. Conflict with search_terms, only use one of them. See list of searchable fields below (same as search_terms).searchTerms
is a dictionary of fields to search in with the following available fields:.id
,.status
,.from
,.to
. Ex:[.from: "someAddress", .id: "someId"]
Then you can call:
Transaction.list(
using: client,
params: params) { (transactionsResult) in
switch transactionsResult {
case .success(let paginatedList):
//TODO: Do something with the paginated list of transactions
case .fail(let error):
//TODO: Handle the error
}
}
The paginatedList
here is an object
Where:
-
data
is an array of transactions -
pagination
is aPagination
objectWhere:
perPage
is the number of results per page.currentPage
is the retrieved page.isFirstPage
is a bool indicating if the page received is the first pageisLastPage
is a bool indicating if the page received is the last page
The SDK offers 2 ways for transferring tokens between addresses:
- A simple one way transfer from one of the current user's wallets to an address.
- A highly configurable send/receive mechanism in 2 steps using transaction requests.
The most basic way to transfer tokens is to use the Transaction.create()
method, which allows the current user to send tokens from one of its wallet to a specific address.
let params = TransactionCreateParams(fromAddress: "1e3982f5-4a27-498d-a91b-7bb2e2a8d3d1",
toAddress: "2e3982f5-4a27-498d-a91b-7bb2e2a8d3d1",
amount: 1000,
tokenId: "BTC:xe3982f5-4a27-498d-a91b-7bb2e2a8d3d1",
idempotencyToken: "some token")
Transaction.create(using: client, params: params) { (result) in
switch result {
case .success(data: let transaction):
// TODO: Do something with the transaction
case .fail(error: let error):
//TODO: Handle the error
}
}
There are different ways to initialize a TransactionCreateParams
by specifying either address
, userId
or accountId
.
A more configurable way to transfer tokens between 2 wallets is to use the transaction request flow.
To make a transaction happen, a TransactionRequest
needs to be created and consumed by a TransactionConsumption
.
To generate a transaction request you can call:
let params = TransactionRequestCreateParams(type: .receive,
tokenId: "a token id",
amount: 1337,
address: "an address",
correlationId: "a correlation id",
requireConfirmation: false,
maxConsumptions: 10,
consumptionLifetime: 60000,
expirationDate: nil,
allowAmountOverride: true,
maxConsumptionsPerUser: 5,
metadata: [:],
encryptedMetadata: [:])!
TransactionRequest.create(using: client, params: params) { (transactionRequestResult) in
switch transactionRequestResult {
case .success(data: let transactionRequest):
//TODO: Do something with the transaction request (get the QR code representation for example)
case .fail(error: let error):
//TODO: Handle the error
}
Where:
-
params
is aTransactionRequestCreateParams
struct constructed using:type
: The QR code type,.receive
or.send
.tokenId
: The id of the desired token. In the case of a type "send", this will be the token taken from the requester. In the case of a type "receive" this will be the token received by the requesteramount
: (optional) The amount of token to receive. This amount can be either inputted when generating or consuming a transaction request.address
: (optional) The address specifying where the transaction should be sent to. If not specified, the current user's primary wallet address will be used.correlationId
: (optional) An id that can uniquely identify a transaction. Typically an order id from a provider.requireConfirmation
: (optional) A boolean indicating if the request needs a confirmation from the requester before being proceededmaxConsumptions
: (optional) The maximum number of time that this request can be consumedconsumptionLifetime
: (optional) The amount of time in millisecond during which a consumption is validexpirationDate
: (optional) The date when the request will expire and not be consumable anymoreallowAmountOverride
: (optional) Allow or not the consumer to override the amount specified in the request. This needs to be true if the amount is not specified
Note that if
amount
is nil andallowAmountOverride
is false the init will fail and returnnil
.maxConsumptionsPerUser
: The maximum number of consumptions allowed per unique usermetadata
: Additional metadata embedded with the requestencryptedMetadata
: Additional encrypted metadata embedded with the request
The previously created transactionRequest
can then be consumed:
let params = TransactionConsumptionParams(transactionRequest: transactionRequest,
address: "an address",
amount: 1337,
idempotencyToken: "an idempotency token",
correlationId: "a correlation id",
metadata: [:],
encryptedMetadata: [:])!
TransactionConsumption.consumeTransactionRequest(using: client, params: params) { (transactionConsumptionResult) in
switch transactionConsumptionResult {
case .success(data: let transactionConsumption):
// Handle success
case .fail(error: let error):
// Handle error
}
}
Where params
is a TransactionConsumptionParams
struct constructed using:
transactionRequest
: The transactionRequest obtained from the QR scanner.address
: (optional) The address from which to take the funds. If not specified, the current user's primary wallet address will be used.amount
: (optional) The amount of token to send. This amount can be either inputted when generating or consuming a transaction request.
Note that if the
amount
was not specified in the transaction request it needs to be specified here, otherwise the init will fail and returnnil
.
idempotencyToken
: The idempotency token used to ensure that the transaction will be executed one time only on the server. If the network call fails, you should reuse the sameidempotencyToken
when retrying the request.correlationId
: (optional) An id that can uniquely identify a transaction. Typically an order id from a provider.metadata
: A dictionary of additional data to be stored for this transaction consumption.encryptedMetadata
: A dictionary of additional encrypted data to be stored for this transaction consumption.
transactionConsumption.approve(using:client, callback: { (result) in
switch result {
case .success(data: let transactionConsumption):
// Handle success
case .fail(error: let error):
// Handle error
}
})
transactionConsumption.reject(using:client, callback: { (result) in
switch result {
case .success(data: let transactionConsumption):
// Handle success
case .fail(error: let error):
// Handle error
}
})
To improve the UX of the transfers, the SDK offers the possibility to generate a QR code from a TransactionRequest
and scan it in order to generate a TransactionConsumption
Once a TransactionRequest
is created you can get its QR code representation using transactionRequest.qrImage()
.
This method takes an optional CGSize
param that can be used to define the expected size of the generated QR image.
To enable QR code scanning, you first need to add the NSCameraUsageDescription
permission in your Info.plist
.
You can then use the integrated QRScannerViewController
to scan the generated QR code.
Initialize the view controller using:
if let vc = QRScannerViewController(delegate: self, client: client, cancelButtonTitle: "Cancel") {
self.present(vc, animated: true, completion: nil)
}
Note: that the initialization of the controller may fail if the device doesn't support video capture (ie: the iOS simulator).
The QRScannerViewControllerDelegate
offers the following interface:
func scannerDidCancel(scanner: QRScannerViewController) {
// Handle tap on cancel button: Typically dismiss the scanner
}
func scannerDidDecode(scanner: QRScannerViewController, transactionRequest: TransactionRequest) {
// Handle success scan, typically consume the transactionRequest and dismiss the scanner
}
func scannerDidFailToDecode(scanner: QRScannerViewController, withError error: OMGError) {
// Handle error
}
When the scanner successfully decodes a TransactionRequest
it will call its delegate method scannerDidDecode(scanner: QRScannerViewController, transactionRequest: TransactionRequest)
.
You should use this TransactionRequest
to generate a TransactionConsumptionParams
in order to consume the request.
This section describes the use of the socket client in order to listen for events for a resource.
Similarly to the HTTP client, the SocketClient
needs to be first initialized with a ClientConfiguration
before using it. The initializer takes an optional SocketConnectionDelegate
delegate which can be used to listen for connection change events (connection and disconnection).
let configuration = ClientConfiguration(baseURL: "wss://your.base.url/api/socket",
apiKey: "apiKey",
authenticationToken: "authenticationToken",
debugLog: false)
let socketClient = SocketClient(config: configuration, delegate: self)
Where:
baseURL
is the URL of the OmiseGO Wallet API, this needs to be an ws(s) url.apiKey
is the API key (typically generated on the admin panel)authenticationToken
is the token corresponding to an OmiseGO Wallet user retrievable using one of our server-side SDKs.
You can find more info on how to retrieve this token in the OmiseGO server SDK documentations.
debugLog
is a boolean indicating if the SDK should print logs in the console.
Some resources are listenable, meaning that a SocketClient
can be used establish a websocket connection and an object conforming to a subclass of the EventDelegate
protocol can be used to listen for events incoming on this resource.
The EventDelegate
protocol contains 3 common methods for all event delegates:
func didStartListening()
func didStopListening()
func onError(_ error: APIError)
didStartListening
can be used to know when the socket channel has been established and is ready to receive events.didStopListening
can be used to know when the socket channel connection is closed and is not receiving events anymore.onError
is called when there is an incoming error object.
And for each of the listenable resource there is an other specific method to receive related events:
When creating a TransactionRequest
that requires a confirmation it is possible to listen for all incoming confirmation using:
transactionRequest.startListeningEvents(withClient: socketClient, eventDelegate: self)
Where:
client
is aSocketClient
eventDelegate
is aTransactionRequestEventDelegate
that will receive incoming events.
An object conforming to TransactionRequestEventDelegate
needs to implement the 3 common methods mentioned above and also:
onTransactionConsumptionRequest(_ transactionConsumption: TransactionConsumption)
.
This method will be called when a TransactionConsumption
is trying to consume the TransactionRequest
.
This allows the requester to confirm or not the consumption if legitimate.
onSuccessfulTransactionConsumptionFinalized(_ transactionConsumption: TransactionConsumption)
.
This method will be called if a TransactionConsumption
has been finalized successfully, and the transfer was made between the 2 wallets.
onFailedTransactionConsumptionFinalized(_ error: APIError)
.
This method will be called if a TransactionConsumption
fails to consume the request.
Similarly to transaction request events, a TransactionConsumption
can be listened for incoming confirmations using:
consumption.startListeningEvents(withClient: socketClient, eventDelegate: self)
Where:
client
is aSocketClient
eventDelegate
is aTransactionConsumptionEventDelegate
that will receive incoming events.
An object conforming to TransactionConsumptionEventDelegate
needs to implement the 3 common methods mentioned above and also:
onSuccessfulTransactionConsumptionFinalized(_ transactionConsumption: TransactionConsumption)
.
This method will be called if the TransactionConsumption
has been finalized successfully, and the transfer was made between the 2 addresses.
onFailedTransactionConsumptionFinalized(_ error: APIError)
.
This method will be called if the TransactionConsumption
fails to consume the request.
A User
can also be listened and will receive all events that are related to him:
user.startListeningEvents(withClient: socketClient, eventDelegate: self)
Where:
client
is aSocketClient
eventDelegate
is aUserEventDelegate
that will receive incoming events.
An object conforming to UserEventDelegate
needs to implement the 3 common methods mentioned above and also on(_ object: WebsocketObject, error: APIError?, forEvent event: SocketEvent)
.
This method will be called when any event regarding the user is received. WebsocketObject
can be enumerated to get the corresponding object received.
When you don't need to receive events anymore, you should call stopListening(withClient client: socketClient)
for the corresponding Listenable
object. This will leave the corresponding socket channel and close the connection if no other channel is active.
In order to run the live tests (bound to a working server) you need to fill the corresponding variables in the plist file secret.plist
.
Note: Alternatively, these keys can be provided with environment variables, making it easier and safer for CI to run since you don't need to keep them in the source code.
The variables are:
OMG_BASE_URL
OMG_WEBSOCKET_URL
OMG_API_KEY
OMG_AUTHENTICATION_TOKEN
OMG_TOKEN_ID
You can then for example run the tests with the following command:
xcodebuild -workspace "OmiseGO.xcworkspace" -scheme "OmiseGO" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8' OMG_BASE_URL="https://your.base.server.url/api" OMG_API_KEY="yourAPIKey" OMG_AUTHENTICATION_TOKEN="yourTestAuthenticationToken" OMG_TOKEN_ID="aTokenId" OMG_WEBSOCKET_URL="wss://your.base.socket.url/api/socket" test
There are two dependencies required to run the SDK.
- Starscream to manage websockets
- BigInt to manage big numbers
See how you can help.
The OmiseGO iOS SDK is released under the Apache License.