-
Jemimah Omodior authored
Include example output in the "Testing With CLI-Generated Contact Files" section of the "Getting Started" doc
Jemimah Omodior authoredInclude example output in the "Testing With CLI-Generated Contact Files" section of the "Getting Started" doc
- Getting Started
- Set Up the Client Locally
- Download an NDF
- Import the API
- Create a Client Object
- Log In to Your Client Session
- Register a Message Listener
- Start Network Threads
- Request Authenticated Channels
- Testing With CLI-Generated Contact Files
- Accept Authenticated Channels
- Send E2E Messages
- Receive Messages
- Putting It All Together
- Next Steps: The API Reference
sidebar_position: 2
Getting Started
To integrate cMix with your application logic, each instance needs to be connected to the cMix network. This begins with creating a new client, then registering the client identity with the xx cMix network. The client session data is stored in an encrypted key-value (EKV) store containing client state and keys.
The rest of this document outlines the steps for building a simple messaging app. It covers the entire process of integrating the cMix Client API (xxDK) in your application, registering within the xxDK and setting up a connection with the cMix network, setting up listeners, as well as sending and receiving messages.
Set Up the Client Locally
The following sections show how to connect to the public xx cMix network. When building your application, it is recommended to test with a local instance of the cMix network.
The command-line tool that comes with the client is useful for testing network functionality. It also comes in handy for acquiring an NDF, a JSON file that describes the Nodes, Gateways, and other servers on the network and how to communicate with them.
:::note
The NDF is required for registering within the xxDK. It can be acquired via the command line or with the DownloadAndVerifySignedNdfWithUrl()
function from the client API.
:::
Here are the commands for cloning and compiling the client (assuming golang 1.17 or newer). You’ll want to make sure to compile the right binary for your specific OS architecture:
git clone https://gitlab.com/elixxir/client.git client
cd client
go mod vendor -v
go mod tidy
go test ./...
# Compile a binary for your specific OS architecture using one of the following commands
# Linux 64 bit binary
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o client.linux64 main.go
# Windows 64 bit binary
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o client.win64 main.go
# Windows 32 bit binary
GOOS=windows GOARCH=386 CGO_ENABLED=0 go build -ldflags '-w -s' -o client.win32 main.go
# Mac OSX 64 bit binary (intel)
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o client.darwin64 main.go
You can pull up the complete usage guide for using the client CLI via the --help
flag:
go run main.go --help
Download an NDF
As noted earlier, you can fetch the NDF either through the command line or using the DownloadAndVerifySignedNdfWithUrl()
API access point. This section describes the steps involved when using the CLI.
The getndf
CLI command enables you to download the NDF from a network gateway. This does not require a pre-established client connection, but you will need the IP address for the gateway, a port, and an SSL certificate.
First, download an SSL certificate from the gateway:
// Download an SSL certificate (assumes you are running a gateway locally)
openssl s_client -showcerts -connect localhost:8440 < /dev/null 2>&1 | openssl x509 -outform PEM > certfile.pem
Next, download the NDF from the gateway:
// Fetch NDF (example usage for Gateways, assumes you are running a gateway locally)
go run main.go getndf --gwhost localhost:8440 --cert certfile.pem | jq . >ndf.json
You can also download an NDF directly for different environments by using the --env
flag
go run main.go getndf --env mainnet | jq . >ndf.json
// Or, run via the binary on 64 bit windows:
./client.win64 getndf --env mainnet | jq . >ndf.json
For more information on the NDF and its structure, see Network Definition File (NDF).
Import the API
The cMix client is designed to be used as a Go library, so you can import it the same as any other Go package:
"gitlab.com/elixxir/client/api"
Note that the code listings below for our sample app assume you have also imported these libraries:
import (
"io/ioutil"
"fmt"
"os"
// external
jww "github.com/spf13/jwalterweatherman" // logging
)
You will need to import a few more packages along the way. However, we want to avoid unused import warnings from the compiler, so we will include them as needed. It is straightforward to switch the external libraries out for any alternatives you prefer.
:::tip
-
To ensure you are using the latest release version of the client, you can run
go get gitlab.com/elixxir/client@release
. This will update yourgo.mod
file automatically. -
It is also important to note, before continuing, that clients need to perform certain actions in a specified order, such as registering specific handlers before starting network threads. Additionally, some actions cannot be performed until connected to the network. The API will return errors saying that the network is not in a healthy state when it is having trouble connecting. :::
Create a Client Object
Creating a new client object will initialize a session directory containing the EKV store that will hold client state. This is done by calling the NewClient()
function:
func NewClient(ndfJSON string, storageDir string, password []byte,
registrationCode string) error
NewClient()
expects multiple arguments:
-
ndfJSON
: The first argument,ndfJSON
, is string-formatted NDF data. -
storageDir
:storageDir
is the path to the directory that will store client state. It is astring
type. -
password
: The third argument,password
, is used to create a user-specified password for accessing their sessions. It is expected to be a byte slice ([]byte
). -
registrationCode
: The final argument,registrationCode
, is optional. It lets pre-selected users register a code with the registration server. Use an empty string to register without a code.
Here is how we have set up NewClient()
in our messaging app:
// You would ideally use a configuration tool to acquire these parameters
statePath := "statePath"
statePass := "password"
// The following connects to mainnet. For historical reasons, it is called a json file
// but it is actually a marshalled file with a cryptographic signature attached.
// This may change in the future.
ndfURL := "https://elixxir-bins.s3.us-west-1.amazonaws.com/ndf/release.json"
certificatePath := "release.crt"
ndfPath := "ndf.json"
// Create the client if there is no session
if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
ndfJSON := ""
if ndfPath != "" {
ndfJSON, err = ioutil.ReadFile(ndfPath)
if err != nil {
jww.WARN.Printf("Could not read NDF: %+v")
}
}
if ndfJSON == "" {
ndfJSON, err := api.DownloadAndVerifySignedNdfWithUrl(ndfURL, certificatePath)
if err != nil {
jww.FATAL.Panicf("Failed to download NDF: %+v", err)
}
}
err = api.NewClient(string(ndfJSON), statePath, []byte(statePass), "")
if err != nil {
jww.FATAL.Panicf("Failed to create new client: %+v", err)
}
}
There are two crucial steps here.
- You need to get an NDF, which you may already have from the Download an NDF step. In the code above, we attempt to read from a file first, then try to download the release NDF with
DownloadAndVerifySignedNdfWithUrl()
, which dynamically downloads the NDF data a client needs from a specified URL. It takes two arguments:-
url
: A publicly accessible URL pointing to the NDF data. -
cert
: The certificate for the scheduling/registration server.
-
:::note There are multiple URL/certificate pairs associated with different environments. It is extremely important to use the correct pair for your specific environment. These include:
For each environment (for example, mainnet), you can download the NDF and extract the signing certificate from the NDF with:
echo -e $(base64 -d mainnet.json | head -2 | tail -1 | tr -dc '[[:print:]]' | jq .Registration.Tls_certificate) | sed 's/\"//g' > ndf.crt
You can also copy and paste the certificates directly from the command line source code. :::
- Once you have an NDF, you can then call the
NewClient()
function. This will create a storage object for user data and generate cryptographic keys. It will also connect and register the client with the cMix network. AlthoughNewClient()
does not register a username, it creates a new user based on a cryptographic identity. That makes it possible to add a username later.
:::note
Aside from the normal client created by NewClient()
, there are other types of clients you can create, such as a precanned client or a vanity client. For instance, a vanity client (NewVanityClient()
) will create a user with a receptionID
that starts with a supplied prefix. This is similar to Bitcoin's vanity addresses.
:::
Log In to Your Client Session
Once you have created a client object, the next step is to load and log in to the session you just initialized. To log in to your client session, you will need to call the Login()
function:
func Login(storageDir string, password []byte, parameters params.Network)
(*Client, error)
The Login()
function expects the same session directory and password used to create the client object. It also expects some network parameters, which you can fetch using the params
interface via params.GetDefaultNetwork()
. To use it, import gitlab.com/elixxir/client/interfaces/params
:
// Login with the same sessionPath and sessionPass used to call NewClient()
// Assumes you have imported "gitlab.com/elixxir/client/interfaces/params"
client, err := api.Login(statePath, []byte(statePass), params.GetDefaultNetwork())
if err != nil {
jww.FATAL.Panicf("Failed to initialize client: %+v", err)
}
Aside from logging you into your existing client session, the Login()
function also initializes communication with the network and registers your client with the permissioning server. This enables you to keep track of network rounds.
To view the current user identity for a client, call the GetUser()
method:
user := client.GetUser()
Register a Message Listener
To acknowledge and reply to received messages, you will need to register a listener. First, import the switchboard package and the message interface:
"gitlab.com/elixxir/client/switchboard"
"gitlab.com/elixxir/client/interfaces/message"
Next, set up the listener. You will also need to create a receiver channel with a suitably large capacity and a type of Message.Receive
:
// Set up a reception handler
swboard := client.GetSwitchboard()
// Note: the receiverChannel needs to be large enough that your reception thread will
// process the messages. If it is too small, messages can be dropped or important xxDK
// threads could be blocked.
receiverChannel := make(chan message.Receive, 10000)
// Note that the name `listenerID` is arbitrary
listenerID := swboard.RegisterChannel("DefaultCLIReceiver",
switchboard.AnyUser(), message.XxMessage, receiverChannel)
jww.INFO.Printf("Message ListenerID: %v", listenerID)
The switchboard from GetSwitchboard()
is used for interprocess signaling about received messages. On the other hand, RegisterChannel()
registers a new listener built around the passed channel (in this case, receiverChannel
). Here is the function signature for RegisterChannel()
:
func (sw *Switchboard) RegisterChannel(name string, user *id.ID,
messageType message.Type, newListener chan message.Receive) ListenerID
RegisterChannel()
expects a name for the listener, a user ID (which you want to set to switchboard.AnyUser()
to listen for messages from any user), a message type, and a channel.
RegisterChannel()
returns a listener ID that you want to keep handy if you need to delete the listener later.
:::caution
Note that we have used swboard
for the variable holding the result of calling client.GetSwitchboard()
, rather than switchboard
. This avoids a clash with the imported switchboard
package, which is used to access the AnyUser()
type.
:::
Start Network Threads
The next step is to start the network follower. A network follower is a thread that keeps track of rounds and network health. As mentioned earlier, the client cannot perform some actions (such as confirming authenticated channels) until the network is in a healthy state.
Generally, the network is in a healthy state when the health tracker sees rounds completed successfully.
To start a network follower, use the StartNetworkFollower()
method:
func (c *Client) StartNetworkFollower(timeout time.Duration) error
StartNetworkFollower()
takes a single argument, a Duration
type (from the time
standard library) which specifies how much time to wait for the function call to succeed before timeout errors are returned. You will want to call StartNetworkFollower()
when your application returns from sleep and close it when going back to sleep.
For our messaging app, we have also set up a function that waits until the network is healthy. Here is our sample code for starting network threads and waiting until the network is healthy:
// Set networkFollowerTimeout to a value of your choice (type is of `time.Duration`)
err = client.StartNetworkFollower(networkFollowerTimeout)
if err != nil {
jww.FATAL.Panicf("Failed to start network follower: %+v", err)
}
waitUntilConnected := func(connected chan bool) {
// Assumes you have imported the `time` package
waitTimeout := time.Duration(150)
timeoutTimer := time.NewTimer(waitTimeout * time.Second)
isConnected := false
// Wait until we connect or panic if we cannot by a timeout
for !isConnected {
select {
case isConnected = <-connected:
jww.INFO.Printf("Network Status: %v\n",
isConnected)
break
case <-timeoutTimer.C:
jww.FATAL.Panic("timeout on connection")
}
}
}
// Create a tracker channel to be notified of network changes
connected := make(chan bool, 10)
// AddChannel() adds a channel to the list of Tracker channels that will be
// notified of network changes34e
client.GetHealth().AddChannel(connected)
// Wait until connected or crash on timeout
waitUntilConnected(connected)
Note that to use the above code listing from our sample app, you will need to have imported the standard time
package. By now, your import section should look like this:
import (
"fmt"
"io/ioutil"
"os"
"time"
"gitlab.com/elixxir/client/api"
"gitlab.com/elixxir/client/interfaces/params"
"gitlab.com/elixxir/client/interfaces/message"
"gitlab.com/elixxir/client/switchboard"
jww "github.com/spf13/jwalterweatherman" // logging
)
In summary, StartNetworkFollower()
kicks off tracking of the network. It starts long-running threads and returns an object for checking state and stopping those threads. However, since these threads may become a significant drain on device batteries when offline, you will want to ensure they are stopped if there is no internet access.
Request Authenticated Channels
There are two ways to send messages across the network: with or without end-to-end (E2E) encryption. Sending messages safely, with E2E encryption, requires an authenticated channel to be established between the communicating parties.
An authenticated channel is a state where both the sender and recipient have each verified their identities. Clients can send and receive authenticated channel requests.
Here is how to request an authenticated channel from another user:
// Get your contact details
me := client.GetUser().GetContact()
roundID, authReqErr := client.RequestAuthenticatedChannel(recipientContact, me, "Hi! Let's connect!")
if authReqErr == nil {
jww.INFO.Printf("Requested auth channel from: %s in round %d",
recipientID, roundID)
} else {
jww.FATAL.Panicf("%+v", err)
}
RequestAuthenticatedChannel()
takes three arguments: the recipient's contact, the sender's contact, and an arbitrary message string:
func (c *Client) RequestAuthenticatedChannel(recipient contact.Contact,
me contact.Contact, message string) (id.Round, error)
Sender and recipient contact details can be acquired for their individual client instances using client.GetUser().GetContact()
, which can be marshaled and written to/read from disk with the .Marshal()
and .Unmarshal(...)
functions.
The RequestAuthenticatedChannel()
method returns the ID for the network round in which the request was sent, or an error if the request was unsuccessful (such as when a channel already exists or if a request was already received).
:::note
RequestAuthenticatedChannel()
will not run if the network state is not healthy. However, it can be retried until it is successful.
:::
In our example above, we used the recipientID
. This can be accessed via client.GetUser().GetContact().ID
. Here is what the Contact data structure (returned by GetContact()
) looks like:
type Contact struct {
ID *id.ID
DhPubKey *cyclic.Int
// The OwnershipProof field is only included for third-party contact data
// such as returned by the GetAuthenticatedChannelRequest() method discussed below
OwnershipProof []byte
Facts fact.FactList
}
Testing With CLI-Generated Contact Files
We are running multiple client instances locally to test out authenticated channels for our messaging app. Although not the most ideal way to get it, this means that we can also fetch the recipient's contact details from CLI-generated contact files:
// Sender's contact
me := client.GetUser().GetContact()
// Recipient's contact (read from a Client CLI-generated contact file)
contactData, _ := ioutil.ReadFile("../user2/user-contact.json")
// Assumes you have imported "gitlab.com/elixxir/crypto/contact"
// which provides an `Unmarshal` function to convert the byte slice ([]byte) output
// of `ioutil.ReadFile()` to the `Contact` type expected by `RequestAuthenticatedChannel()`
recipientContact, _ := contact.Unmarshal(contactData)
recipientID := recipientContact.ID
roundID, authReqErr := client.RequestAuthenticatedChannel(recipientContact, me, "Hi! Let's connect!")
if authReqErr == nil {
jww.INFO.Printf("Requested auth channel from: %s in round %d",
recipientID, roundID)
} else {
jww.FATAL.Panicf("%+v", err)
}
To generate a contact file (such as user-contact.json
above) via the CLI, use the --writeContact
flag. You’ll want to send an unsafe (without E2E encryption) message to yourself using the following command, where user-password
is your password and session-directory
is the path to the directory that will store your client state:
# You may need to use the `--waitTimeout` flag to avoid timeout errors
# For example, `--waitTimeout 200` (time in seconds)
./client.win64 --password user-password --ndf ndf.json -l client.log -s session-directory --writeContact user-contact.json --unsafe -m "Hello World, without E2E Encryption" --waitTimeout 200
Sending to yYAztmoCoAH2VIr00zPxnj/ZRvdiDdURjdDWys0KYI4D: Hello World, without E2E Encryption
Message received: Hello World, without E2E Encryption
Received 1
Note that when duplicating folders to create multiple client instances locally, you need to ensure you are not also copying over contact files and session folders. You can comfortably delete session folders since each new NewClient()
call will generate new cryptographic identities, but only if there isn't an existing session.
Accept Authenticated Channels
Although we can now send authenticated channel requests, we have yet to be able to accept them. To give your application the ability to react to requests for authenticated channels, you first need to register a handler:
// Set up auth request handler
authManager := client.GetAuthRegistrar()
authManager.AddGeneralRequestCallback(authChannnelCallbackFunction)
Calling the GetAuthRegistrar()
method on the client returns a manager object which allows registration of authentication callbacks. Then you can use the AddGeneralRequestCallback()
method on this object to register your handler.
Here is an example showing how to register a handler that simply prints the user ID of the requestor:
// Simply prints the user id of the requestor.
func printChanRequest(requestor contact.Contact) {
msg := fmt.Sprintf("Authentication channel request from: %s\n",
requestor.ID)
jww.INFO.Printf(msg)
fmt.Printf(msg)
msg = fmt.Sprintf("Authentication channel request message: %s\n", message)
jww.INFO.Printf(msg)
fmt.Printf(msg)
}
// Set up auth request handler.
authManager := client.GetAuthRegistrar()
authManager.AddGeneralRequestCallback(printChanRequest)
Let's see another example with a callback that first checks if a channel already exists for a recipient before confirming it automatically:
confirmChanRequest := func(requestor contact.Contact, message string) {
// Check if a channel exists for this recipientID
recipientID := requestor.ID
if client.HasAuthenticatedChannel(recipientID) {
jww.INFO.Printf("Authenticated channel already in place for %s",
recipientID)
return
}
// GetAuthenticatedChannelRequest returns the contact received in a request if
// one exists for the given userID. Returns an error if no contact is found.
recipientContact, err := client.GetAuthenticatedChannelRequest(recipientID)
if err == nil {
jww.INFO.Printf("Accepting existing channel request for %s",
recipientID)
// ConfirmAuthenticatedChannel() creates an authenticated channel out of a valid
// received request and informs the requestor that their request has
// been confirmed
roundID, err := client.ConfirmAuthenticatedChannel(recipientContact)
fmt.Println("Accepted existing channel request in round ", roundID)
jww.INFO.Printf("Accepted existing channel request in round %v",
roundID)
if err != nil {
jww.FATAL.Panicf("%+v", err)
}
return
}
}
authManager := client.GetAuthRegistrar()
authManager.AddGeneralRequestCallback(confirmChanRequest)
Note that just like RequestAuthenticatedChannel()
, ConfirmAuthenticatedChannel()
will not run if the network state is not healthy. It also returns an error if a channel already exists, if a request does not exist, or if the contact passed in does not match the contact received in a request.
ConfirmAuthenticatedChannel()
can also be retried (such as in cases where the network was initially unhealthy).
Send E2E Messages
Sending encrypted payloads requires an authenticated channel to be established between sender and recipient. Assuming that is the case, here is how to construct and send an e2e message:
// Send safe message with authenticated channel, requires an authenticated channel
// Test message
msgBody := "If this message is sent successfully, we'll have established first contact with aliens."
msg := message.Send{
Recipient: recipientID,
Payload: []byte(msgBody),
MessageType: message.XxMessage,
}
// Get default network parameters for E2E payloads
paramsE2E := params.GetDefaultE2E()
fmt.Printf("Sending to %s: %s\n", recipientID, msgBody)
roundIDs, _, _, err := client.SendE2E(msg,
paramsE2E)
if err != nil {
jww.FATAL.Panicf("%+v", err)
}
jww.INFO.Printf("Message sent in RoundIDs: %+v\n", roundIDs)
There are three steps involved when sending messages:
-
Generate message: Sent messages have a
message.Send
type. You will need to include a recipient ID, the message payload (which should be a byte slice), and a message type (which should bemessage.XxMessage
). -
Get default network parameters: Next, you will need to get the default network parameters using
params.GetDefaultE2E()
. This again assumes you previously importedgitlab.com/elixxir/client/interfaces/params
. -
Send message: Finally, you can send your message using
client.SendE2E()
. This will return the list of rounds in which parts of your message were sent or an error if the call was unsuccessful.
:::note
In addition to the round IDs and error message, client.SendE2E()
returns two additional items: the message ID and the timestamp for when the message was sent.
:::
Receive Messages
We set up a listener earlier for incoming messages. However, go run
terminates almost immediately after it is executed. So, we want to make sure to keep our receiving channel open and test that we can actually see received messages:
// Set `waitTimeout` to a value of your choice
timeoutTimer := time.NewTimer(waitTimeout * time.Second)
select {
case <-timeoutTimer.C:
fmt.Println("Timed out!")
break
case msg := <-receiverChannel:
fmt.Printf("Message received: %s\n", string(
msg.Payload))
break
}
This will terminate your app after one message is received. Alternatively, you can use a loop to keep it running:
for {
msg := <-receiverChannel
fmt.Println(string(msg.Payload))
}
We have had to format our received messages via string(msg.Payload)
in both cases. As you might recall from when we sent messages earlier, the payload is a byte slice.
Received messages have a type of Message.Receive
and are structured this way:
type Receive struct {
ID e2e.MessageID
Payload []byte
MessageType Type
Sender *id.ID
RecipientID *id.ID
EphemeralID ephemeral.Id
RoundId id.Round
RoundTimestamp time.Time
Timestamp time.Time
Encryption EncryptionType
}
In addition to the payload, you can access other details such as the sender's ID, their recipient ID, the round ID in which the message was sent, the timestamp for when the sender sent the message, and more.
:::info
The EphemeralID
is the time-based identity used to receive the message from the network. The sender generates an EphemeralID
using the recipient’s RecipientID
, then encrypts it in the packet that is sent over the network. Finally, the recipient polls the servers with their ephemeral IDs to pick up messages.
Ephemeral IDs, by design, overlap and conflict with other recipients to hide who receives what messages. Because they are temporal, different recipients conflict with each other over time. :::
Putting It All Together
On a high level, integrating the Client API with your application can be reduced to:
- Generate an identity for your client and use this to connect to the cMix network.
- Keep track of the network using network followers.
- Register listeners for messages and authenticated channel requests.
- Send and receive messages with or without encryption.
You will want to ensure that you perform these actions in the expected order, such as starting a network follower before registering a handler for authenticated channel requests.
Here is what our Go app (main.go
) currently looks like, with most of our code samples so far reproduced here:
package main
import (
"fmt"
"io/ioutil"
"os"
"time"
"gitlab.com/elixxir/client/api"
"gitlab.com/elixxir/client/interfaces/message"
"gitlab.com/elixxir/client/interfaces/params"
"gitlab.com/elixxir/client/switchboard"
"gitlab.com/elixxir/crypto/contact"
"gitlab.com/xx_network/primitives/id"
// external
jww "github.com/spf13/jwalterweatherman" // logging
)
func main() {
// Create a new client object-------------------------------------------------------
// You would ideally use a configuration tool to acquire these parameters
statePath := "statePath"
statePass := "password"
// The following connects to mainnet. For historical reasons it is called a json file
// but it is actually a marshalled file with a cryptographic signature attached.
// This may change in the future.
ndfURL := "https://elixxir-bins.s3.us-west-1.amazonaws.com/ndf/release.json"
certificatePath := "release.crt"
ndfPath := "ndf.json"
// Create the client if there is no session
if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
ndfJSON := ""
if ndfPath != "" {
content, err := ioutil.ReadFile(ndfPath)
if err != nil {
jww.WARN.Printf("Could not read NDF: %+v")
}
}
if ndfJSON == "" {
ndfJSON, err := api.DownloadAndVerifySignedNdfWithUrl(ndfURL, certificatePath)
if err != nil {
jww.FATAL.Panicf("Failed to download NDF: %+v", err)
}
}
err = api.NewClient(string(ndfJSON), statePath, []byte(statePass), "")
if err != nil {
jww.FATAL.Panicf("Failed to create new client: %+v", err)
}
}
// Login to your client session-----------------------------------------------------
// Login with the same sessionPath and sessionPass used to call NewClient()
// Assumes you have imported "gitlab.com/elixxir/client/interfaces/params"
client, err := api.Login(statePath, []byte(statePass), params.GetDefaultNetwork())
if err != nil {
jww.FATAL.Panicf("Failed to initialize client: %+v", err)
}
// view current user identity--------------------------------------------------------
user := client.GetUser()
fmt.Println(user)
// Register a listener for messages--------------------------------------------------
// Set up a reception handler
swboard := client.GetSwitchboard()
// Note: the receiverChannel needs to be large enough that your reception thread will
// process the messages. If it is too small, messages can be dropped or important xxDK
// threads could be blocked.
receiverChannel := make(chan message.Receive, 10000)
// Note that the name `listenerID` is arbitrary
listenerID := swboard.RegisterChannel("DefaultCLIReceiver",
switchboard.AnyUser(), message.XxMessage, receiverChannel)
jww.INFO.Printf("Message ListenerID: %v", listenerID)
// Start network threads------------------------------------------------------------
// Set networkFollowerTimeout to an int64 of your choice (seconds)
err = client.StartNetworkFollower(networkFollowerTimeout)
if err != nil {
jww.FATAL.Panicf("Failed to start network follower: %+v", err)
}
waitUntilConnected := func(connected chan bool) {
// Assumes you have imported the `time` package
waitTimeout := time.Duration(150)
timeoutTimer := time.NewTimer(waitTimeout * time.Second)
isConnected := false
// Wait until we connect or panic if we cannot by a timeout
for !isConnected {
select {
case isConnected = <-connected:
jww.INFO.Printf("Network Status: %v\n",
isConnected)
break
case <-timeoutTimer.C:
jww.FATAL.Panic("timeout on connection")
}
}
}
// Create a tracker channel to be notified of network changes
connected := make(chan bool, 10)
// AddChannel() adds a channel to the list of Tracker channels that will be
// notified of network changes34e
client.GetHealth().AddChannel(connected)
// Wait until connected or crash on timeout
waitUntilConnected(connected)
// Register a handler for authenticated channel requests-----------------------------
// Handler for authenticated channel requests
confirmChanRequest := func(requestor contact.Contact, message string) {
// Check if a channel exists for this recipientID
recipientID := requestor.ID
if client.HasAuthenticatedChannel(recipientID) {
jww.INFO.Printf("Authenticated channel already in place for %s",
recipientID)
return
}
// GetAuthenticatedChannelRequest returns the contact received in a request if
// one exists for the given userID. Returns an error if no contact is found.
recipientContact, err := client.GetAuthenticatedChannelRequest(recipientID)
if err == nil {
jww.INFO.Printf("Accepting existing channel request for %s",
recipientID)
// ConfirmAuthenticatedChannel() creates an authenticated channel out of a valid
// received request and informs the requestor that their request has
// been confirmed
roundID, err := client.ConfirmAuthenticatedChannel(recipientContact)
fmt.Println("Accepted existing channel request in round ", roundID)
jww.INFO.Printf("Accepted existing channel request in round %v",
roundID)
if err != nil {
jww.FATAL.Panicf("%+v", err)
}
return
}
}
// Register `confirmChanRequest` as the handler for auth channel requests
authManager := client.GetAuthRegistrar()
authManager.AddGeneralRequestCallback(confirmChanRequest)
// Request auth channels from other users---------------------------------------------
// Sender's contact for requesting auth channels
me := client.GetUser().GetContact()
// Recipient's contact (read from a Client CLI-generated contact file)
contactData, _ := ioutil.ReadFile("../user2/user-contact.json")
// Assumes you have imported "[gitlab.com/elixxir/crypto/contact](http://gitlab.com/elixxir/crypto/contact)"
// which provides an `Unmarshal` function to convert the byte slice ([]byte) output
// of `ioutil.ReadFile()` to the `Contact` type expected by `RequestAuthenticatedChannel()`
recipientContact, _ := contact.Unmarshal(contactData)
recipientID := recipientContact.ID
roundID, authReqErr := client.RequestAuthenticatedChannel(recipientContact, me, "Hi! Let's connect!")
if authReqErr == nil {
jww.INFO.Printf("Requested auth channel from: %s in round %d",
recipientID, roundID)
} else {
jww.FATAL.Panicf("%+v", err)
}
// Send a message to another user----------------------------------------------------
// Send safe message with authenticated channel, requires an authenticated channel
// Test message
msgBody := "If this message is sent successfully, we'll have established first contact with aliens."
msg := message.Send{
Recipient: recipientID,
Payload: []byte(msgBody),
MessageType: message.XxMessage,
}
// Get default network parameters for E2E payloads
paramsE2E := params.GetDefaultE2E()
fmt.Printf("Sending to %s: %s\n", recipientID, msgBody)
roundIDs, _, _, err := client.SendE2E(msg,
paramsE2E)
if err != nil {
jww.FATAL.Panicf("%+v", err)
}
jww.INFO.Printf("Message sent in RoundIDs: %+v\n", roundIDs)
// Keep app running to receive messages-----------------------------------------------
for {
msg := <-receiverChannel
fmt.Println(string(msg.Payload))
}
}
If you would like to duplicate this app to simulate multiple users, you can simply comment out irrelevant lines when you need to switch between sending and receiving messages or requesting and accepting authenticated channels.
The sample app is also available on Gitlab.
Next Steps: The API Reference
For more comprehensive information on using the Client API, see the API reference docs.