-
-
Save orodio/a74293f65e83145ec8b968294808cf35 to your computer and use it in GitHub Desktop.
In FCL wallets are configured by passing in a wallet providers authentication URL to the discovery.wallet
config.
You as someone who is making an FCL compatible wallet will need to make and expose an authentication endpoint that will be rendered in an iframe.
// 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-render-in-iframe"
})
Once the page is rendered and ready to do things, the wallet needs to tell FCL that it is ready, you will do this by sending a post message to FCL, and FCL will send one back with some extra info you can use to know about the application requesting authentication on behalf of the user.
// IN WALLET AUTHENTICATION FRAME
function callbackback({ data }) {
// early exit if it isnt the FCL:AUTHN:CONFIG message
if (typeof data != "object") return
if (typeof data.type !== "FCL:AUTHN:CONFIG") return
// this might have a bunch of usefull information
doSomethingWithConfigData(e.data)
// always clean up your eventlisteners
window.removeEventListener(callback)
}
// add event listener first
window.addEventListener("message", callback)
// tell fcl the wallet is ready
window.parent.postMessage({type: "FCL:FRAME:READY"}, "*")
You will learn fairly fast that almost everything in FCL is optional, the config that FCL will send here is the applications chance to suggest to you the wallet what they would like you to send them. An example of that is the OpenID service, the application can via that config request for you the wallet to send them the email address of the current user, them requesting this does not mean you need to send it. In the config they can also tell you the wallet a couple things about them the application like the name of their application or a url for an icon so you can customize things slightly. You the wallet having a visual distinction but a seemless and connected experience is our goal here. As always never trust anything you recieve from an application, always do your due-dilligence and be onguard, you as the wallet are the users first line of defense.
At this point its important that you as the wallet are confident that the user is the user. Have them provide enough proof to you that you are okay with passing their details back to FCL. If we were to use Blocto as an example, this is where the user is actually loggin into Blocto, putting in their email, getting sent the code, everything Blocto needs to do to be confident in the users identity.
Once you confident in the users identity, we can complete this authentication process. The authentication process is complete once FCL receives back a post message that configures it for the user. This response is everything to FCL, at its core it tells FCL who the user is and then via included services it includes how the user authenticated, how to request transaction signatures, how to get a personal message signed, the email and other details if requested, and in the future it may inlude many more things. You can kind of think of FCL as a plugin system, but all the plugins exist elsewhere, so FCL needs to communicate with those plugins. What you are sending back to FCL is everything that it needs to communicate with the plugins you are supplying. Your wallet is a plugin to FCL, these details tell FCL how to use you as a plugin.
// IN WALLET AUTHENTICATION FRAME
window.postMessage({
type: "FCL:FRAME:RESPONSE", // The message type FCL is expecting
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-render-in-iframe", // 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: "help@aw.com", // 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: "bob",
family_name: "builder", // icky underscored names because of OpenID Connect spec
given_name: "robert",
middle_name: "the",
nickname: "bob the builder",
preferred_username: "bob",
profile: "https://www.bobthebuilder.com/",
picture: "https://avatars.onflow.org/avatar/bob-the-builder",
website: "https://www.bobthebuilder.com",
gender: "small cartoonish man",
birthday: "1999-01-30", // can use 0000 for year if year is not known
zoneinfo: "America/Vancouver", // they are so inconsistent :(
locale: "en",
updated_at: "1625588304427"
},
email: {
email: "bob@bob.bob",
email_verified: false,
}
},
}
]
}, "*")
From any frame, you can send a FCL:FRAME:CLOSE
message to FCL which will halt the current routine and close the frame.
window.parent.postMessage({ type: "FCL:FRAME:CLOSE" }, "*")
In the above Authenitaction section you will have been introduced to services.
Services are your main way of configuring FCL.
Sometimes they just configure it and thats it, an example of that can be seen with the Authn Service and the OpenID Service.
With those two services you as the wallet are just saying "here is a bunch of info".
You will see that those two services both have a method: "DATA"
field in them.
Currently in those two case they can only be a data service.
Other services can be a little more complex, they can require a back and forth between FCL and the Service in question. Ultimately we want to do this back and forth via a secure back-channel, http requests to servers, but in some situations that isn't a viable option so there is also a front-channel option. Where possible you as a wallet provider should aim to provide a back-channel support for services, and only fall back to a front-channel if absolutely necessary.
Back-channel communication use method: "HTTP/POST"
, while front-channel communication use method: "IFRAME/RPC"
.
IFRAME/RPC
is the easiest to explain so we will start with that.
You have more or less already been doing this with authentication (we will eventually enable back-channel for authentication).
- An iframe is rendered (comes from
endpoint
in the service). - The rendered frames says its ready
window.parent.postMessage({type: "FCL:FRAME:READY"}, "*")
. - FCL will send the data to be dealt with
frame.postMessage({ type: "FCL:FRAME:READY:RESPONSE", ...body, service: {params, data} }, "*")
- Where
body
is the stuff you care about,params
anddata
are things 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 its approved the polling responses data field will need to be what FCL is expecting
- If its 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 imediately return a f_type: "PollingResponse"
.
Like the IFRAME/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
).
When the polling response is PENDING
it requires and update
field that includes a service (BackChannelRpc
) 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
or DECLINED
FCL will halt and return/error, but if it is PENDING
it will use the BackChannelRpc
supplied in the new PollingResponse
update field.
It will repeat this cycle until it is either APPROVED
or DECLINED
.
There is an additional feature that HTTP/POST
enables in the first PollingResponse
that is returned.
This feature is the ability for FCL to render an iframe and it can be triggered by supplying a service type: "FRAME"
and the endpoint
that the wallet wishes to render.
// 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",
type: "frame",
endpoint: "https://____", // the iframe that will be rendered,
data: {}, // sent to frame when ready
params: {}, // included as query params on endpoint
}
}
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:FRAME:READY:RESPONSE
for anIFRAME/RPC
Authorization services are depicted with with a type: "authz"
and a method
of either HTTP/POST
or IFRAME/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`
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 a 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, tag the message, and then sign it producing a signature.
The signature needs to be sent back to FCL as a HEX string in the response object (which is a CompositeSignature
).
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"
}
}
User Signature services are depicted with a type: "user-signature"
and a method
of either HTTP/POST
or IFRAME/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 (Wrapperd 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
.
# per required signature
signature =
signable.message
|> tag
|> sign
|> convert_to_hex
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 the wallet is responsible for multiple roles of a transaction but want 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
or IFRAME/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
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 eventaul 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",
}
],
}
}