Skip to content
Snippets Groups Projects
Commit 1f07400d authored by Richard T. Carback III's avatar Richard T. Carback III
Browse files

Beginnings of new API, starting with definitions in bindings

parent 9522e939
No related branches found
No related tags found
No related merge requests found
# Supporting Elixxir payments # Client Bindings
### Protocol buffer crash course for Elixxir "bindings" is the client bindings which can be used to generate Android
and iOS client libraries for apps using gomobile. Gomobile is
#### Why use protocol buffers? limited to int, string, []byte, interfaces, and only a couple other types, so
it is necessary to define several interfaces to support passing more complex
Protocol buffer definitions (written in a .proto file) can generate code for serializing and deserializing byte sequences in a huge variety of languages. This means that, if you decide to standardize your message format with a protocol buffer declaration, you can decode and encode the messages in any language without having to interact with the Go client library at all. Just as important is the ability to define enumerations in protocol buffers. In particular, the message types that are currently used for the CUI and the command-line client are defined in the Types enum in `client/cmixproto/types.proto`. If you have any questions about the way that the data are serialized for any message type, you should read this file and its comments. data across the boundary (see `interfaces.go`). The rest of the logic
is located in `api.go`
#### Generating protocol buffer code
To generate the code, use the `protoc` tool, requesting output in the target language of your choice. For Go:
`protoc --go_out=. types.proto`
For Java:
`protoc --java_out=/path/to/android/project/source/code types.proto`
You can download and install the protocol buffer compiler for your preferred language from [its downloads page](https://developers.google.com/protocol-buffers/docs/downloads).
#### Message types used to implement payments UI
The payments portion of the user interface should only need to register its listeners with the wallet. To get the wallet (currently there's only one), call `Bindings.getActiveWallet()`. You can listen to the wallet with `Bindings.getActiveWallet().Listen(...)`. When the client has received a message that the UI needs to respond to, it should listen to the wallet for messages of these types: `PAYMENT_INVOICE_UI`, `PAYMENT_RESPONSE`, and `PAYMENT_RECEIPT_UI`. See cmixproto/types.proto for some documentation about these file formats.
### What must client implementers do for the minimal payment implementation?
There are three parties that must participate to complete a payment: the payer, the payee, and the payment bots. Payer and payee are both normal, human users using some Elixxir client program, and the payment bots are automatic users that arbitrate exchanges of which tokens have value.
A payment happens like this: payee sends invoice with some payee-owned tokens that don't yet have value, payer receives invoice and decides to pay it, payer sends payment message to bots with payee-owned tokens and some tokens of his own that are worth the same and have value, bots locally see that the payer's tokens have value, bots store value in the payee's new tokens and destroy the payer's old tokens, bots reply to the payer with a message saying that the payment was successful, payer responds to the payee with a message asserting that they made the payment.
Under the hood, the client library updates a lot of state to perform the necessary cryptography to update the wallet to its correct state. At the time, though, the mechanism for rolling back failed transactions is relatively untested and needs some ironing out. In the meantime, if a transaction fails and further transactions are messed up, you should restart the whole server infrastructure--server, gateway, and payment bot--to reset the tokens that are available on the payment bot, and wipe the stored sessions of the clients to register with a fresh set of tokens.
In short, if you're implementing a client, your client must be able to do the following things to deliver the baseline payments experience:
- send an invoice to another user on the payee's client
- receive and display an incoming invoice on the payer's client
- pay the received invoice on the payer's client
- receive and display the payment bots' response on the payer's client
- receive and display the payer's receipt on the payee's client
How to do each of these things with the current payments API follows. Assume that `w` is a reference or pointer to the active wallet. Assume that `CmixProto` is an imported package with proto buffer code generated in Java. The code is written in Java-like pseudocode. Of course, you should structure your own code in the best way for your own application.
#### 0. Actually having tokens to spend
To actually have tokens to spend, you must mint the same tokens as are on the payment bot. Currently, the tokens are hard-coded. To do this, pass `true` to the last parameter of `Bindings.Register()`. Then, there will be tokens that are stored in the wallet that happen to be the same as the tokens that are stored on the payment bot (when the payment bot is run with `--mint`), and the client will be able to spend them.
#### 1. Send an invoice to another user on the payee's client
First, generate the invoice, then send it.
```java
public static void sendInvoice() throws Throwable {
// Generate the invoice message: Request 500 tokens from the payer
Message invoiceMessage = w.Invoice(payerId.bytes(), 500,
"for creating a completely new flavor of ice cream");
// Send the invoice message to the payer
Bindings.send(invoiceMessage);
}
```
#### 2. Receive and display an incoming invoice on the payer's client
During client startup, register a listener with the wallet. The wallet has a separate listener matching structure from the main switchboard, and you can use it to receive messages from the wallet rather than from the network.
```java
public static void setup() throws Throwable {
Bindings.InitClient(...);
Bindings.Register(...);
// The wallet and listener data structure (switchboard) are both ready after Register.
// On the other hand, the client begins receiving messages after Login. So, if
// you want to receive all messages that the client receives, it's best to register
// listeners between Register and Login.
registerListeners();
Bindings.Login(...);
}
public static void registerListeners() {
// Listen for messages of type PAYMENT_INVOICE_UI originating from all users
w.Listen(zeroId.bytes(), CmixProto.Type.PAYMENT_INVOICE_UI, new PaymentInvoiceListener());
// and so on...
}
```
The message you'll get contains an invoice ID. You can use it to query the invoice's transaction for display. You should also store the invoice ID as it's the parameter for the Pay method, the next phase.
```java
public class PaymentInvoiceListener implements Bindings.Listener {
@Override
public void Hear(Bindings.Message msg, bool isHeardElsewhere) {
// Keep this ID around somewhere for the next step
byte[] invoiceID = msg.GetPayload();
bindings.Transaction invoice = w.GetInboundRequests.Get(invoiceID);
// Display the transaction somehow
invoiceDisplay.setTime(invoice.timestamp);
invoiceDisplay.setValue(invoice.value);
invoiceDisplay.setMemo(invoice.memo);
invoiceDisplay.show();
}
}
```
#### 3. Pay the received invoice on the payer's client
When the payer approves the invoice's payment, call the Pay() method with the invoice ID. This will generate a message that the payer can send to the payment bot. The message contains the payee's tokens, which the payment bot will vest, and the payer's proof of ownership of tokens of equal value, which the payment will destroy to facilitate the exchange.
```java
public static void sendPayment() throws Throwable {
Message msg = Bindings.pay(invoiceID);
Bindings.send(msg);
}
```
#### 4. Receive and display the payment bots' response on the payer's client
When you register listeners, listen to the wallet for the type `PAYMENT_RESPONSE`.
```java
public static void registerListeners() {
// ...
w.Listen(zeroId.bytes(), CmixProto.Type.PAYMENT_RESPONSE, new PaymentResponseListener());
// ...
}
```
The payment response is a serialized protocol buffer with a few fields. You should deserialize it using the protocol buffer code you generated.
```java
public class PaymentResponseListener implements Binding.Listener {
@Override
public void Hear(Bindings.Message msg, bool isHeardElsewhere) {
// Parse the payment bot's response
PaymentResponse response = PaymentResponse.parseFrom(msg.GetPayload());
// Then, show it to the user somehow
responseDisplay.setSuccess(response.getSuccess());
responseDisplay.setText(response.getResponse());
responseDisplay.show();
}
}
```
The client automatically converts the payment bot's response to a receipt and sends it on to the payee.
#### 5. Receive and display the payer's receipt on the payee's client
When you register listeners, listen to the wallet for the type `PAYMENT_RECEIPT_UI`.
```java
public static void registerListeners() {
// ...
w.Listen(zeroId.bytes(), CmixProto.Type.PAYMENT_RECEIPT_UI, new PaymentReceiptListener());
// ...
}
```
The payment receipt UI message is the ID of the original invoice that was paid. You can get the transaction itself from the CompletedOutboundPayments transaction list, then display the transaction information of the completed payment.
```java
public class PaymentReceiptListener implements Bindings.Listener {
@Override
public void Hear(bindings.Message msg, bool isHeardElsewhere) {
// Get the relevant transaction
bindings.Transaction completedTransaction = w.getCompletedOutboundTransaction(msg.GetPayload());
// Show the receipt, including the time that the original invoice was sent.
receiptDisplay.setTime(completedTransaction.time);
receiptDisplay.setMemo(completedTransaction.memo);
receiptDisplay.setValue(completedTransaction.value);
receiptDisplay.show();
}
}
```
### Cyclic group for registration JSON format:
```json
{
"gen": "5c7ff6b06f8f143fe8288433493e4769c4d988ace5be25a0e24809670716c613d7b0cee6932f8faa7c44d2cb24523da53fbe4f6ec3595892d1aa58c4328a06c46a15662e7eaa703a1decf8bbb2d05dbe2eb956c142a338661d10461c0d135472085057f3494309ffa73c611f78b32adbb5740c361c9f35be90997db2014e2ef5aa61782f52abeb8bd6432c4dd097bc5423b285dafb60dc364e8161f4a2a35aca3a10b1c4d203cc76a470a33afdcbdd92959859abd8b56e1725252d78eac66e71ba9ae3f1dd2487199874393cd4d832186800654760e1e34c09e4d155179f9ec0dc4473f996bdce6eed1cabed8b6f116f7ad9cf505df0f998e34ab27514b0ffe7",
"prime": "9db6fb5951b66bb6fe1e140f1d2ce5502374161fd6538df1648218642f0b5c48c8f7a41aadfa187324b87674fa1822b00f1ecf8136943d7c55757264e5a1a44ffe012e9936e00c1d3e9310b01c7d179805d3058b2a9f4bb6f9716bfe6117c6b5b3cc4d9be341104ad4a80ad6c94e005f4b993e14f091eb51743bf33050c38de235567e1b34c3d6a5c0ceaa1a0f368213c3d19843d0b4b09dcb9fc72d39c8de41f1bf14d4bb4563ca28371621cad3324b6a2d392145bebfac748805236f5ca2fe92b871cd8f9c36d3292b5509ca8caa77a2adfc7bfd77dda6f71125a7456fea153e433256a2261c6a06ed3693797e7995fad5aabbcfbe3eda2741e375404ae25b",
"primeQ": "f2c3119374ce76c9356990b465374a17f23f9ed35089bd969f61c6dde9998c1f"
}
```
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2019 Privategrity Corporation /
// /
// All rights reserved. /
////////////////////////////////////////////////////////////////////////////////
package bindings
import (
"gitlab.com/elixxir/client/api"
)
// NewClient connects and registers to the network using a json encoded
// network information string and then creates a new client at the specified
// storageDir using the specified password. This function will fail
// when:
// - network information cannot be read or the client cannot connect
// to the network and register within the defined timeout.
// - storageDir does not exist and cannot be created
// - It cannot create, read, or write files inside storageDir
// - Client files already exist inside storageDir.
// - cryptographic functionality is unavailable (e.g. random number
// generation)
// The password is passed as a byte array so that it can be cleared from
// memory and stored as securely as possible using the memguard library.
// NewClient will block until the client has completed registration with
// the network permissioning server.
func NewClient(network, storageDir string, password []byte) (Client, error) {
// TODO: This should wrap the bindings ClientImpl, when available.
return api.NewClient(network, storageDir, password)
}
// LoadClient will load an existing client from the storageDir
// using the password. This will fail if the client doesn't exist or
// the password is incorrect.
// The password is passed as a byte array so that it can be cleared from
// memory and stored as securely as possible using the memguard library.
// LoadClient does not block on network connection, and instead loads and
// starts subprocesses to perform network operations.
func LoadClient(storageDir string, password []byte) (Client, error) {
// TODO: This should wrap the bindings ClientImpl, when available.
return api.LoadClient(storageDir, password)
}
...@@ -11,14 +11,133 @@ import ( ...@@ -11,14 +11,133 @@ import (
"gitlab.com/elixxir/primitives/switchboard" "gitlab.com/elixxir/primitives/switchboard"
) )
// Message used for binding // Client is defined inside the api package. At minimum, it implements all of
// functionality defined here. A Client handles all network connectivity, key
// generation, and storage for a given cryptographic identity on the cmix
// network.
type Client interface {
// ----- Reception -----
// RegisterListener records and installs a listener for messages
// matching specific uid, msgType, and/or username
RegisterListener(uid []byte, msgType int, username string,
listener Listener)
// ----- Transmission -----
// SendE2E sends an end-to-end payload to the provided recipient with
// the provided msgType. Returns the list of rounds in which parts of
// the message were sent or an error if it fails.
SendE2E(payload, recipient []byte, msgType int) (RoundList, error)
// SendUnsafe sends an unencrypted payload to the provided recipient
// with the provided msgType. Returns the list of rounds in which parts
// of the message were sent or an error if it fails.
// NOTE: Do not use this function unless you know what you are doing.
// This function always produces an error message in client logging.
SendUnsafe(payload, recipient []byte, msgType int) (RoundList, error)
// SendCMIX sends a "raw" CMIX message payload to the provided
// recipient. Note that both SendE2E and SendUnsafe call SendCMIX.
// Returns the round ID of the round the payload was sent or an error
// if it fails.
SendCMIX(payload, recipient []byte) (int, error)
// ----- Notifications -----
// RegisterForNotifications allows a client to register for push
// notifications.
// Note that clients are not required to register for push notifications
// especially as these rely on third parties (i.e., Firebase *cough*
// *cough* google's palantir *cough*) that may represent a security
// risk to the user.
RegisterForNotifications(token []byte) error
// UnregisterForNotifications turns of notifications for this client
UnregisterForNotifications() error
// ----- Registration -----
// Returns true if the cryptographic identity has been registered with
// the CMIX user discovery agent.
// Note that clients do not need to perform this step if they use
// out of band methods to exchange cryptographic identities
// (e.g., QR codes), but failing to be registered precludes usage
// of the user discovery mechanism (this may be preferred by user).
IsRegistered() bool
// RegisterIdentity registers an arbitrary username with the user
// discovery protocol. Returns an error when it cannot connect or
// the username is already registered.
RegisterIdentity(username string) error
// RegisterEmail makes the users email searchable after confirmation.
// It returns a registration confirmation token to be used with
// ConfirmRegistration or an error on failure.
RegisterEmail(email string) ([]byte, error)
// RegisterPhone makes the users phone searchable after confirmation.
// It returns a registration confirmation token to be used with
// ConfirmRegistration or an error on failure.
RegisterPhone(phone string) ([]byte, error)
// ConfirmRegistration sends the user discovery agent a confirmation
// token (from Register Email/Phone) and code (string sent via Email
// or SMS to confirm ownership) to confirm ownership.
ConfirmRegistration(token, code []byte) error
// ----- Contacts -----
// GetUser returns the current user Identity for this client. This
// can be serialized into a byte stream for out-of-band sharing.
GetUser() (Contact, error)
// MakeContact creates a contact from a byte stream (i.e., unmarshal's a
// Contact object), allowing out-of-band import of identities.
MakeContact(contactBytes []byte) (Contact, error)
// GetContact returns a Contact object for the given user id, or
// an error
GetContact(uid []byte) (Contact, error)
// ----- User Discovery -----
// Search accepts a "separator" separated list of search elements with
// an associated list of searchTypes. It returns a ContactList which
// allows you to iterate over the found contact objects.
Search(data, separator string, searchTypes []byte) ContactList
// SearchWithHandler is a non-blocking search that also registers
// a callback interface for user disovery events.
SearchWithHandler(data, separator string, searchTypes []byte,
hdlr UserDiscoveryHandler)
// ----- Key Exchange -----
// CreateAuthenticatedChannel creates a 1-way authenticated channel
// so this user can send messages to the desired recipient Contact.
// To receive confirmation from the remote user, clients must
// register a listener to do that.
CreateAuthenticatedChannel(recipient Contact, payload []byte) error
// RegierAuthEventsHandler registers a callback interface for channel
// authentication events.
RegisterAuthEventsHandler(hdlr AuthEventHandler)
// ----- Network -----
// StartNetworkRunner kicks off the longrunning network client threads
// and returns an object for checking state and stopping those threads.
// Call this when returning from sleep and close when going back to
// sleep.
StartNetworkRunner() NetworkRunner
// RegisterRoundEventsHandler registers a callback interface for round
// events.
RegisterRoundEventsHandler(hdlr RoundEventHandler)
}
// Message is a message received from the cMix network in the clear
// or that has been decrypted using established E2E keys.
type Message interface { type Message interface {
// Returns the message's sender ID // Returns the message's sender ID, if available
GetSender() []byte GetSender() []byte
// Returns the message payload // Returns the message payload/contents
// Parse this with protobuf/whatever according to the type of the message // Parse this with protobuf/whatever according to the message type
GetPayload() []byte GetPayload() []byte
// Returns the message's recipient ID // Returns the message's recipient ID
// This is usually your userID but could be an ephemeral/group ID
GetRecipient() []byte GetRecipient() []byte
// Returns the message's type // Returns the message's type
GetMessageType() int32 GetMessageType() int32
...@@ -28,88 +147,77 @@ type Message interface { ...@@ -28,88 +147,77 @@ type Message interface {
GetTimestampNano() int64 GetTimestampNano() int64
} }
// Copy of the storage interface. // RoundEvent contains event information for a given round.
// It is identical to the interface used in Globals, // TODO: This is a half-baked interface and will be filled out later.
// and a results the types can be passed freely between the two type RoundEvent interface {
type Storage interface { // GetID returns the round ID for this round.
// Give a Location for storage. Does not need to be implemented if unused. GetID() int
SetLocation(string, string) error // GetStatus returns the status of this round.
// Returns the Location for storage. GetStatus() int
// Does not need to be implemented if unused.
GetLocation() string
// Stores the passed byte slice to location A
SaveA([]byte) error
// Returns the stored byte slice stored in location A
LoadA() []byte
// Stores the passed byte slice to location B
SaveB([]byte) error
// Returns the stored byte slice stored in location B
LoadB() []byte
// Returns whether the storage has even been written to.
// if something exists in A or B
IsEmpty() bool
} }
// Translate a bindings storage to a client storage // ContactList contains a list of contacts
type storageProxy struct { type ContactList interface {
boundStorage Storage // GetLen returns the number of contacts in the list
GetLen() int
// GetContact returns the contact at index i
GetContact(i int) Contact
} }
// Translate a bindings message to a parse message // Contact contains the contacts information. Note that this object
// is a copy of the contact at the time it was generated, so api users
// cannot rely on this object being updated once it has been received.
type Contact interface {
// GetID returns the user ID for this user.
GetID() []byte
// GetPublicKey returns the publickey bytes for this user.
GetPublicKey() []byte
// GetSalt returns the salt used to initiate an authenticated channel
GetSalt() []byte
// IsAuthenticated returns true if an authenticated channel exists for
// this user so we can begin to send messages.
IsAuthenticated() bool
// IsConfirmed returns true if the user has confirmed the authenticated
// channel on their end.
IsConfirmed() bool
// Marshal creates a serialized representation of a contact for
// out-of-band contact exchange.
Marshal() ([]byte, error)
}
// ----- Callback interfaces -----
// Listener provides a callback to hear a message
// An object implementing this interface can be called back when the client // An object implementing this interface can be called back when the client
// gets a message of the type that the registerer specified at registration // gets a message of the type that the registerer specified at registration
// time. // time.
type Listener interface { type Listener interface {
// This does not include the generic interfaces seen in the go implementation // Hear is called to receive a message in the UI
// Those are used internally on the backend and cause errors if we try to port them Hear(msg Message)
Hear(msg Message, isHeardElsewhere bool)
}
// Translate a bindings listener to a switchboard listener
// Note to users of this package from other languages: Symbols that start with
// lowercase are unexported from the package and meant for internal use only.
type listenerProxy struct {
proxy Listener
}
func (lp *listenerProxy) Hear(msg switchboard.Item, isHeardElsewhere bool, i ...interface{}) {
msgInterface := &parse.BindingsMessageProxy{Proxy: msg.(*parse.Message)}
lp.proxy.Hear(msgInterface, isHeardElsewhere)
}
// Interface used to receive a callback on searching for a user
type SearchCallback interface {
Callback(userID, pubKey []byte, err error)
}
type searchCallbackProxy struct {
proxy SearchCallback
}
func (scp *searchCallbackProxy) Callback(userID, pubKey []byte, err error) {
scp.proxy.Callback(userID, pubKey, err)
}
// Interface used to receive a callback on searching for a user's nickname
type NickLookupCallback interface {
Callback(nick string, err error)
}
type nickCallbackProxy struct {
proxy NickLookupCallback
} }
// interface used to receive the result of a nickname request // AuthEventHandler handles authentication requests initiated by
func (ncp *nickCallbackProxy) Callback(nick string, err error) { // CreateAuthenticatedChannel
ncp.proxy.Callback(nick, err) type AuthEventHandler interface {
// HandleConfirmation handles AuthEvents received after
// the client has called CreateAuthenticatedChannel for
// the provided contact. Payload is typically empty but
// may include a small introductory message.
HandleConfirmation(contact Contact, payload []byte)
// HandleRequest handles AuthEvents received before
// the client has called CreateAuthenticatedChannel for
// the provided contact. It should prompt the user to accept
// the channel creation "request" and, if approved,
// call CreateAuthenticatedChannel for this Contact.
HandleRequest(contact Contact, payload []byte)
} }
// interface used to receive a ui friendly description of the current status of // RoundEventHandler handles round events happening on the cMix network.
// registration type RoundEventHandler interface {
type ConnectionStatusCallback interface { HandleEvent(re RoundEvent)
Callback(status int, TimeoutSeconds int)
} }
type OperationProgressCallback interface { // UserDiscoveryHandler handles search results against the user discovery agent.
Callback(int) type UserDiscoveryHandler interface {
HandleSearchResults(results ContactList)
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment