- Last Updated: August 18th 2021
- Stable: Yes
- Risk of Breaking Change: Medium
- Compatibility:
>= @onflow/[email protected]
This document is written with the perspective that you who are reading this right now are an FCL Wallet Developer. All references to you in this doc are done with this perspective in mind.
Flow Client Library (FCL) approaches the idea of blockchain wallets on Flow in a different way than how wallets may be supported on other blockchains. For example, with FCL, a wallet is not necessarily limited to being a browser extention or even a native application on a users device. FCL offers wallet developers the flexibility and freedom to build many different types of applications. Since wallet applications can take on many forms, we needed to create a way for these varying applications to be able to communicate and work together.
FCL acts in many ways as a protocol to facilitate communication and configuration between the different parties involved in a blockchain application. An Application can use FCL to authenticate users, and request authorizations for transactions, as well as mutate and query the Blockchain. An application using FCL offers it's Users a way to connect and select any number of Wallet Providers and their Wallet Services. A selected Wallet provides an Application's instance of FCL with configuration information about itself and its Wallet Services, allowing the User and Application to interact with them.
In the following paragraphs we'll explore ways in which you can integrate with FCL by providing implementataions of various FCL services.
The following services will be covered:
- Authentication (Authn) Service
- Authorization (Authz) Service
- User Signature Service
- Pre-Authz Service
FCL Services are your way as a Wallet Provider of configuring FCL with information about what your wallet can do. FCL uses what it calls Service Methods
to perform your supported FCL services. Services Methods are the ways FCL can talk to your wallet. Your wallet gets to decide which of these service methods each of your supported services use to communicate with you.
Sometimes services just configure FCL and that's it. An example of this can be seen with the Authentication Service and the OpenID Service.
With those two services you are simply telling FCL "here is a bunch of info about the current user". (You will see that those two services both have a method: "DATA"
field in them.
Currently these are the only two cases that can be a data service.)
Other services can be a little more complex. For example, they might require a back and forth communication between FCL and the Service in question. Ultimately we want to do this back and forth via a secure back-channel (https requests to servers), but in some situations that isn't a viable option, so there is also a front-channel option. Where possible, you should aim to provide a back-channel support for services, and only fall back to a front-channel if absolutely necessary.
Back-channel communications use method: "HTTP/POST"
, while front-channel communications use method: "IFRAME/RPC"
, method: "POP/RPC"
or method: "TAB/RPC
.
Service Method | Front | Back |
---|---|---|
HTTP/POST | ⛔ | ✅ |
IFRAME/RPC | ✅ | ⛔ |
POP/RPC | ✅ | ⛔ |
TAB/RPC | ✅ | ⛔ |
It's important to note that regardless of the method of communication, the data that is sent back and forth between the parties involved is the same.
IFRAME/RPC
is the easiest to explain, so we will start with it:
- An iframe is rendered (comes from the
endpoint
in the service). - The rendered frame says its ready
WalletUtils.sendMsgToFCL("FCL:VIEW:READY")
. - FCL will send the data to be dealt with:
WalletUtils.onMsgFromFCL("FCL:VIEW:READY:RESPONSE", ({ body, params, data }) => { ... })
- Where
body
is the stuff you care about,params
anddata
are additional information you can provide in the service object.
- Where
- The wallet sends back an Approved or Declined post message. (It will be a
f_type: "PollingResponse"
, which we will get to in a bit)- If it's approved, the polling responses data field will need to be what FCL is expecting.
- If it's declined, the polling responses reason field should say why it was declined.
POP/RPC
works in an almost entirely similar way to IFRAME/RPC
, except instead of rendering the method
in an iframe, we render it in a popup. The same communication protocol between the rendered view and FCL applies:
- A popup is rendered (comes from
endpoint
in the service). - The rendered popup says its ready
WalletUtils.sendMsgToFCL("FCL:VIEW:READY")
. - FCL will send the data to be dealt with
WalletUtils.onMsgFromFCL("FCL:VIEW:READY:RESPONSE", ({ body, params, data }) => { ... })
- Where
body
is the stuff you care about,params
anddata
are additional information you can provide in the service object.
- Where
- The wallet sends back an Approved or Declined post message (It will be a
f_type: "PollingResponse"
, we will get to that in a bit)- If it's approved, the polling responses data field will need to be what FCL is expecting.
- If it's declined, the polling responses reason field should say why it was declined.
TAB/RPC
works in an almost entirely similar way to IFRAME/RPC
and POP/RPC
, except instead of rendering the method
in an iframe or a popup, we render it in a new tab. The same communication protocol between the rendered view and FCL applies:
- A new tab is rendered (comes from
endpoint
in the service). - The rendered tab says its ready
WalletUtils.sendMsgToFCL("FCL:VIEW:READY")
. - FCL will send the data to be dealt with
WalletUtils.onMsgFromFCL("FCL:VIEW:READY:RESPONSE", ({ body, params, data }) => { ... })
- Where
body
is the stuff you care about,params
anddata
are additional information you can provide in the service object.
- Where
- The wallet sends back an Approved or Declined post message (It will be a
f_type: "PollingResponse"
, we will get to that in a bit)- If it's approved, the polling responses data field will need to be what FCL is expecting.
- If it's declined, the polling responses reason field should say why it was declined.
HTTP/POST
initially sends a post request to the endpoint
specified in the service, which should immediately return a f_type: "PollingResponse"
.
Like IFRAME/RPC
, POP/RPC
or TAB/RPC
, our goal is to eventually get an APPROVED
or DECLINED
polling response, and technically this endpoint could return one of those immediately.
But more than likely that isn't the case and it will be in a PENDING
state (PENDING
is not available to IFRAME/RPC
, POP/RPC
or TAB/RPC
).
When the polling response is PENDING
it requires an updates
field that includes a service, BackChannelRpc
, that FCL can use to request an updated PollingResponse
from.
FCL will use that BackChannelRpc
to request a new PollingResponse
which itself can be APPROVED
, DECLINED
or PENDING
.
If it is APPROVED
FCL will return, otherwise if it is DECLINED
FCL will error. However, if it is PENDING
, it will use the BackChannelRpc
supplied in the new PollingResponse
updates field. It will repeat this cycle until it is either APPROVED
or DECLINED
.
There is an additional optional feature that HTTP/POST
enables in the first PollingResponse
that is returned.
This optional feature is the ability for FCL to render an iframe, popup or new tab, and it can be triggered by supplying a service type: "VIEW/IFRAME"
, type: "VIEW/POP"
or type: "VIEW/TAB"
and the endpoint
that the wallet wishes to render in the local
field of the PollingResponse
. This is a great way for a wallet provider to switch to a webpage if displaying a UI is necessary for the service it is performing.
Each response back to FCL must be "wrapped" in a Polling Response. Each Polling Response can have it's status as "APPROVED"
, "DECLINED"
, or "PENDING"
.
It is entirely acceptible for your service to immediately return an "APPROVED"
Polling Reponse, skipping a "PENDING"
state.
"DECLINED"
Polling Responses must include a human readable reason for why it was declined.
// APPROVED
{
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "APPROVED",
data: ___, // what the service needs to send to FCL
}
// Declined
{
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "DECLINED",
reason: "Declined by user."
}
// Pending - Simple
{
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "PENDING",
updates: {
f_type: "Service",
f_vsn: "1.0.0",
type: "back-channel-rpc",
endpoint: "https://____", // where post request will be sent
method: "HTTP/POST",
data: {}, // will be included in the requests body
params: {}, // will be included in the requests url
}
}
// Pending - First Time with Local
{
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "PENDING",
updates: {
f_type: "Service",
f_vsn: "1.0.0",
type: "back-channel-rpc",
endpoint: "https://____", // where post request will be sent
method: "HTTP/POST",
data: {}, // included in body of request
params: {}, // included as query params on endpoint
},
local: {
f_type: "Service",
f_vsn: "1.0.0",
endpoint: "https://____", // the iframe that will be rendered,
method: "VIEW/IFRAME",
data: {}, // sent to frame when ready
params: {}, // included as query params on endpoint
}
}
A PollingResponse
can alternatively be constructed using WalletUtils
when sending APPROVED
or DECLINED
responses.
import {WalletUtils} from "@onflow/fcl"
// Approving a PollingResponse
// Example using an AuthnResponse as the PollingResponse data
WalletUtils.approve({
f_type: "AuthnResponse",
f_vsn: "1.0.0"
...
})
// Rejecting a PollingResponse
// Supplys a reason for declining
const reason = "User declined to authenticate."
WalletUtils.decline(reason)
data
and params
are information that the wallet can provide in the service config that FCL will pass back to the service.
params
will be added onto theendpoint
as query params.data
will be included in the body of theHTTP/POST
request or in theFCL:VIEW:READY:RESPONSE
for aIFRAME/RPC
,POP/RPC
orTAB/RPC
.
In the following examples, we'll walk you through the process of building an authentication service.
In FCL, wallets are configured by passing in a wallet provider's authentication URL as the discovery.wallet
config variable.
You will need to make and expose a webpage or API hosted at an authentication endpoint that FCL will use.
// IN APPLICATION
// configuring fcl to point at a wallet looks like this
import {config} from "@onflow/fcl"
config({
"discovery.wallet": "your-url-that-fcl-will-use-for-authentication",
"discovery.wallet.method": "IFRAME/RPC" // Optional. Available methods are "IFRAME/RPC", "POP/RPC", "TAB/RPC" or "HTTP/POST", defaults to "IFRAME/RPC".
})
If the method specified is IFRAME/RPC
, POP/RPC
or TAB/RPC
, then the URL specified as discovery.wallet
will be rendered as a webpage. Otherwise, if the method specified is HTTP/POST
, then the authentication process will happen over HTTP requests. (While authentication can be accomplished using any of those service methods, this example will use the IFRAME/RPC
service method.)
Once the Authentication webpage is rendered, or the API is ready, you then need to tell FCL that it is ready. You will do this by sending a message to FCL, and FCL will send back a message with some additional information that you can use about the application requesting authentication on behalf of the user.
The following example is using the IFRAME/RPC
method. Your authentication webpage will likely resemble the following code:
// IN WALLET AUTHENTICATION FRAME
import {WalletUtils} from "@onflow/fcl"
function callback(data) {
if (typeof data != "object") return
if (typeof data.type !== "FCL:VIEW:READY:RESPONSE") return
... // Do authentication things ...
// Send back AuthnResponse
WalletUtils.sendMsgToFCL("FCL:VIEW:RESPONSE", {
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "APPROVED",
data: {
f_type: "AuthnResponse",
f_vsn: "1.0.0"
...
}
})
// The same AuthnResponse can alternatively be sent using WalletUtils.approve (or WalletUtils.decline)
WalletUtils.approve({
f_type: "AuthnResponse",
f_vsn: "1.0.0"
...
})
}
// add event listener first
WalletUtils.onMsgFromFCL("FCL:VIEW:READY:RESPONSE", callback)
// tell fcl the wallet is ready
WalletUtils.sendMsgToFCL("FCL:VIEW:READY")
During authentication, the application has a chance to request to you what they would like you to send back to them. These requests are included in the FCL:VIEW:READY:RESPONSE
messsage sent to the wallet from FCL.
An example of such a request is the OpenID service. The application can request for example that you to send them the email address of the current user. The application requesting this information does not mean you need to send it. It's entirely optional for you to do so. However, some applications may depened on you sending the requested informaation back, and should you decline to do so it may cause the application to not work.
In the config they can also tell you a variety of things about them, such as the name of their application or a url for an icon of their application. You can use these pieces of information to customize your wallet's user experience should you desire to do so.
Your wallet having a visual distinction from the application, but still a seamless and connected experience is our goal here.
Whether your authentication process happens using a webpage with the IFRAME/RPC
, POP/RPC
or TAB/RPC
methods, or using a backchannel to an API with the HTTP/POST
method, the handshake is the same. The same messages are sent in both methods, however the transport mechanism changes. For IFRAME/RPC
, POP/RPC
or TAB/RPC
methods, the transport is window.postMessage()
, with the HTTP/POST
method, the tranport is HTTP post messages.
As always, you must never trust anything you receive from an application. Always do your due-dilligence and be alert as you are the users first line of defense against potentially malicious applications.
It's important that you are confident that the user is who the user claims to be.
Have them provide enough proof to you that you are okay with passing their details back to FCL. Using Blocto as an example, an authentication code is sent to the email a user enters at login. This code can be used as validation and is everything Blocto needs to be confident in the users identity.
Once you're confident in the users identity, we can complete the authentication process.
The authentication process is complete once FCL receives back a response that configures FCL with FCL Services for the current user. This response is extremeley important to FCL. At its core it tells FCL who the user is, and then via the included services it tells FCL how the user authenticated, how to request transaction signatures, how to get a personal message signed and the user's email and other details if requested. In the future it may also inlude many more things!
You can kind of think of FCL as a plugin system. But since those plugins exist elsewhere outside of FCL, FCL needs to be configured with information on how to communicate with them.
What you are sending back to FCL is everything that it needs to communicate with the plugins that you are supplying. Your wallet is like a plugin to FCL, and these details tell FCL how to use you as a plugin.
Here is an example of an authentication resonse:
// IN WALLET AUTHENTICATION FRAME
import {WalletUtils} from "@onflow/fcl"
WalletUtils.approve({
f_type: "AuthnResponse",
f_vsn: "1.0.0",
addr: "0xUSER", // The users flow address
services: [ // All the stuff that configures FCL
// Authentication Service - REQUIRED
{
f_type: "Service", // Its a service!
f_vsn: "1.0.0", // Follows the v1.0.0 spec for the service
type: "authn", // the type of service it is
method: "DATA", // Its data!
uid: "amazing-wallet#authn", // A unique identifier for the service
endpoint: "your-url-that-fcl-will-use-for-authentication", // should be the same as was passed into the config
id: "0xUSER", // the wallets internal id for the user, use flow address if you dont have one
// The Users Info
identity: {
f_type: "Identity", // Its an Identity!
f_vsn: "1.0.0", // Follows the v1.0.0 spec for an identity
address: "0xUSER", // The users address
keyId: 0, // OPTIONAL - The Users KeyId they will use
},
// The Wallets Info
provider: {
f_type: "ServiceProvider", // Its a Service Provider
f_vsn: "1.0.0", // Follows the v1.0.0 spec for service providers
address: "0xWallet", // A flow address owned by the wallet
name: "Amazing Wallet", // OPTIONAL - The name of your wallet. ie: "Dapper Wallet" or "Blocto Wallet"
description: "The best wallet", // OPTIONAL - A short description for your wallet
icon: "https://___", // OPTIONAL - Image url for your wallets icon
website: "https://___", // OPTIONAL - Your wallets website
supportUrl: "https://___", // OPTIONAL - An url the user can use to get support from you
supportEmail: "[email protected]", // OPTIONAL - An email the user can use to get support from you
},
},
// Authorization Service
{
f_type: "Service",
f_vsn: "1.0.0",
type: "authz",
uid: "amazing-wallet#authz",
...
// We will cover this at length in the authorization section of this guide
},
// User Signature Service
{
f_type: "Service",
f_vsn: "1.0.0",
type: "user-signature",
uid: "amazing-wallet#user-signature",
...
// We will cover this at length in the user signature section of this guide
},
// OpenID Service
{
f_type: "Service",
f_vsn: "1.0.0",
type: "open-id",
uid: "amazing-wallet#open-id",
method: "DATA",
data: { // only include data that was request, ideally only if the user approves the sharing of data, everything is optional
f_type: "OpenID",
f_vsn: "1.0.0",
profile: {
name: "Jeff",
family_name: "D", // icky underscored names because of OpenID Connect spec
given_name: "Jeffrey",
middle_name: "FakeMiddleName",
nickname: "JeffJeff",
preferred_username: "Jeff",
profile: "https://www.jeff.jeff/",
picture: "https://avatars.onflow.org/avatar/jeff",
website: "https://www.jeff.jeff/",
gender: "male",
birthday: "1900-01-01", // can use 0000 for year if year is not known
zoneinfo: "America/Vancouver",
locale: "en",
updated_at: "1625588304427"
},
email: {
email: "[email protected]",
email_verified: false,
}
},
}
]
})
From any frame, you can send a FCL:VIEW:CLOSE
post message to FCL, which will halt FCL's current routine and close the frame.
import {WalletUtils} from "@onflow/fcl"
WalletUtils.sendMsgToFCL("FCL:VIEW:CLOSE")
Authorization services are depicted with with a type: "authz"
, and a method
of either HTTP/POST
, IFRAME/RPC
, POP/RPC
or TAB/RPC
.
They are expected to eventually return a f_type: "CompositeSignature"
.
An authorization service is expected to know the Account and the Key that will be used to sign the transaction at the time the service is sent to FCL (during authentication).
{
f_type: "Service",
f_vsn: "1.0.0",
type: "authz", // say its an authorization service
uid: "amazing-wallet#authz", // standard service uid
method: "HTTP/POST", // can also be `IFRAME/RPC` or `POP/RPC`
endpoint: "https://____", // where to talk to the service
identity: {
f_type: "Identity",
f_vsn: "1.0.0",
address: "0xUser", // the address that the signature will be for
keyId: 0, // the key for the address that the signature will be for
},
data: {},
params: {},
}
FCL will use the method
provided to request an array of composite signature from authorization service (Wrapped in a PollingResponse
).
The authorization service will be sent a Signable
.
The service is expected to construct an encoded message to sign from Signable.voucher
.
It then needs to hash the encoded message, and prepend a required transaction domain tag.
Finally it signs the payload with the user/s keys, producing a signature.
This signature, as a HEX string, is sent back to FCL as part of the CompositeSignature
which includes the user address and keyID in the data property of a PollingResponse
.
siganture =
signable.voucher
|> encode
|> hash
|> tag
|> sign
|> convert_to_hex
The eventual response back from the authorization service should resolve to something like this:
{
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "APPROVED",
data: {
f_type: "CompositeSignature",
f_vsn: "1.0.0",
addr: "0xUSER",
keyId: 0,
signature: "signature as hex value"
}
}
A CompositeSignature
can alternatively be constructed using WalletUtils
import {WalletUtils} from "@onflow/fcl"
WalletUtils.CompositeSignature(addr: String, keyId: Number, signature: Hex)
User Signature services are depicted with a type: "user-signature"
and a method
of either HTTP/POST
, IFRAME/RPC
or POP/RPC
.
They are expected to eventually return an array of f_type: "CompositeSignature"
.
The User Signature service is a stock/standard service.
{
f_type: "Service",
f_vsn: "1.0.0",
type: "user-signature", // say its an user-signature service
uid: "amazing-wallet#user-signature", // standard service uid
method: "HTTP/POST", // can also be `IFRAME/RPC`
endpoint: "https://___", // where to talk to the service
data: {},
params: {},
}
FCL will use the method
provided to request an array of composite signatures from the user signature service (Wrapped in a PollingResponse
).
The user signature service will be sent a Signable
.
The service is expected to tag the Signable.message
and then sign it with enough keys to produce a full weight.
The signatures need to be sent back to FCL as HEX strings in an array of CompositeSignatures
.
// Pseudocode:
// For every required signature
import {WalletUtils} from "@onflow/fcl"
const encoded = WalletUtils.encodeMessageFromSignable(signable, signerAddress)
const taggedMessage = tagMessage(encoded) // Tag the messsage to sign
const signature = signMessage(taggedMessage) // Sign the message
const hexSignature = signatureToHex(signature) // Convert the signature to hex, if required.
return hexSignature
The eventual response back from the user signature service should resolve to something like this:
{
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "APPROVED",
data: [
{
f_type: "CompositeSignature",
f_vsn: "1.0.0",
addr: "0xUSER",
keyId: 0,
signature: "signature as hex value"
},
{
f_type: "CompositeSignature",
f_vsn: "1.0.0",
addr: "0xUSER",
keyId: 1,
signature: "signature as hex value"
}
]
}
This is a strange one, but extremely powerful. This service should be used when a wallet is responsible for an account that's signing as multiple roles of a transaction, and wants the ability to change the accounts on a per role basis.
Pre Authz Services are depicted with a type: "pre-authz"
and a method
of either HTTP/POST
, IFRAME/RPC
or POP/RPC
.
They are expected to eventually return a f_type: "PreAuthzResponse"
.
The Pre Authz Service is a stock/standard service.
{
f_type: "Service",
f_vsn: "1.0.0",
type: "pre-authz", // say its a pre-authz service
uid: "amazing-wallet#pre-authz", // standard service uid
method: "HTTP/POST", // can also be IFRAME/RPC, POP/RPC, TAB/RPC
endpoint: "https://___", // where to talk to the service
data: {},
params: {},
}
FCL will use the method
provided to request a PreAuthzReponse
(Wrapped in a PollingResponse
).
The Authorizations service will be sent a PreSignable
.
The pre-authz service is expected to look at the PreSignable
and determine the breakdown of accounts to be used.
The pre-authz service is expected to return Authz
services for each role it is responsible for.
A pre-authz service can only supply roles it is responsible for.
If a pre-authz service is responsible for multiple roles, but it wants the same account to be responsible for all the roles, it will need to supply an Authz service per role.
The eventual response back from the pre-authz service should resolve to something like this:
{
f_type: "PollingResponse",
f_vsn: "1.0.0",
status: "APPROVED",
data: {
f_type: "PreAuthzResponse",
f_vsn: "1.0.0",
proposer: { // A single Authz Service
f_type: "Service",
f_vsn: "1.0.0",
type: "authz",
...
},
payer: [ // An array of Authz Services
{
f_type: "Service",
f_vsn: "1.0.0",
type: "authz",
...
}
],
authorization: [ // An array of Authz Serivces (its singular because it only represents a singular authorization)
{
f_type: "Service",
f_vsn: "1.0.0",
type: "authz",
...
}
],
}
}
FCL employs the following data structures, of which you have previously seen in use throughout this document.