Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FLIP: FCL Tx Payer Service #612

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 331 additions & 0 deletions flips/20210824-fcl-tx-payer-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
# FCL Transaction Payer Service

| Status | Proposed |
:-------------- |:---------------------------------------------------- |
| **FLIP #** | [612](https://github.com/onflow/flow/pull/612) |
| **Author(s)** | Jeffrey Doyle ([email protected]), |
| **Sponsor** | |
| **Updated** | 2020-09-03 |

## Objective

FCL provides a mechanism for a transactions fees to be paid by an account
JeffreyDoyle marked this conversation as resolved.
Show resolved Hide resolved
other than the proposer or any of the authorizers of a transaction.

A Transaction Payer Service would be a service that facilitates signing as the
payer for a given transaction. It would exist as a resusable codebase / set of
packages & tools that could be used by the applications that desire it.

## Motivation

One of the features of Flow is that transaction fees can be paid by an account
separate from the authorizers of that transaction. Applications that wish to
provide "free" transactions for their users could opt to pay for their users'
transactions themselves. They would do this by producing a signature for the
transaction as the payer for that transaction. Upon receiving this transaction,
the Flow network would then deduct transaction fees from the payer, not any of
the authorizers.

Some FCL compatible wallets choose to act as the payer of the transactions their users
sign. They do this by swapping out their users account with an account of their own,
if their users are specified as the payer of a transaction. This allows wallets with
this feature to provide "free" transactions for their users.

While this makes for a desirable user experience for those that use
wallets with this feature, applications do not get the guarantee that _all_ of their
users will get this user experience. This is because users are able to use whichever FCL
compatible wallet they desire with the FCL compatible applications they use, and not
all FCL compatible wallets will strictly choose to provide this feature.

Applications that wish to guarantee their users can submit transactions
without having to pay transaction fees for them will then have to pay those fees themselves.
Comment on lines +100 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly confusing... how about:

Suggested change
Applications that wish to guarantee their users can submit transactions
without having to pay transaction fees for them will then have to pay those fees themselves.
Applications that wish to guarantee their users can submit transactions
without having to pay transaction fees will have to pay those fees in the users' stead.

To pay transaction fees, it's expected that applications would need to build a Transaction Payer Service that is able
to receive a transaction signable and then produce a signature for it.

A re-usable Transaction Payer Service would exist as project / set of packages and tools that applications could use
instead of having to build an implementation of them themselves. These re-usable components
would lower the overall complexity and effort required of a developer who wishes that
their application provide "free" transactions for their users.

## User Benefit

Most consumer applications that we interact with on a daily basis charge us nothing.
As internet users, we're freely able to interact for our favourite social media apps,
interact with our favourite websites and simply do as we please without paying for anything.
However, with blockchain applications, each one of those interactions would likely be
transaction, and with that, transaction fees would have to be paid.

Users have been conditioned to expect things for free. As such, applications that wish
to provide the best user experiences will likely choose to allow their users to execute
transactions without them having to pay.

This Transaction Fee Payer Service will enable more applications to be more easily able to provide
a higher level of user experience.
JeffreyDoyle marked this conversation as resolved.
Show resolved Hide resolved

## Design Proposal

> Disclaimer: The following is an example implementation for what _might_ work. All possible solutions are not limited to just the following. You're completely free and encouraged to approach the problem however you choose, even if it differs extensively from the thoughts below.

When creating a transasction to be sent to the blockchain, FCL expects authorization functions
for each of the roles for that transaction to be specified.

For example, when specifying to FCL that the "Current User" of the application
should be an Authorizer, the Proposer and the Payer of a transaction, it might
look something like:

```javascript
// IN CLIENT APPLICATIONS FRONTEND

import * as fcl from "@onflow/fcl"

const tx = await fcl.send([
fcl.transaction`... Cadence Code ...`,
fcl.authorizers([fcl.currentUser().authorization]),
fcl.proposer(fcl.currentUser().authorization),
fcl.payer(fcl.currentUser().authorization)
]).then(fcl.decode)
```

The line `fcl.currentUser().authorization` is an "Authorization Function".

(Read More about Authorization Functions: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md)

The implementation of this Authorization Function is what FCL uses to gather a
signature from the Current User of the application for each of the roles of the
transaction (Proposer, Payer and Authorizer).

A Transaction Fee Payer Service would need to make available an Authorization Function
that applications could use in place of `fcl.currentUser().authorization` (or whatever other
authorization function they use).

The applications transaction code might then look something like:

```javascript
// IN CLIENT APPLICATIONS FRONTEND

// Note: "TPS" <=> "Transaction Payer Service"

import * as fcl from "@onflow/fcl"
import { TPSAuthorizationFunction } from "@TPS/TPS-client"

const configuredTPSAuthzFn = TPSAuthorizationFunction({
... TPS Configuration ...
resolveAccountURL: "https://api.myawesomedapp.com/tps/resolveaccount" // Example
signingURL: "https://api.myawesomedapp.com/tps/sign" // Example
})

const tx = await fcl.send([
fcl.transaction`... Cadence Code ...`,
fcl.authorizers([fcl.currentUser().authorization]),
fcl.proposer(fcl.currentUser().authorization),
fcl.payer(configuredTPSAuthzFn)
]).then(fcl.decode)
```

The implementation of `TPSAuthorizationFunction` will need to conform to how FCL expects
Authorization Functions to work. (Read More about Authorization Functions: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md)

The implementation of the `TPSAuthorizationFunction` might look something like:

```javascript
// IN "@TPS/TPS-client"

export const TPSAuthorizationFunction = ({ resolveAccountURL, signingURL }) => async (account) => {

/**
Perform a network call to resolve the FCL "account" data structure. This network call will likely be to the client applcation's backend.

The call graph might look something like:

Client App TPSAuthorizationFunction =POST=> Client Backend =POST Response=> Client App TPSAuthorizationFunction

The TPS API would need to return back an account data structure containing:
(See: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md#how-to-create-an-authorization-function for more information on the purpose of each field)
{
...account,
tempId: tpsTempID
addr: tpsPayerAddress
keyId: tpsPayerKeyID
}
**/
const resolvedAccount = await fetch(resolveAccountURL, {
method: "POST",
headers: { "Content-Type": "application/json" }
body: JSON.stringify(account)
}).then(res => res.json())

// The authorization function returns an FCL account data structure with more fields filled in.
return {
...resolvedAccount,
signingFunction: async signable => {
/**
Perform a network call to resolve the transaction signature. This network call will likely be to the client applcation's backend.

The call graph might look something like:

Client App TPSAuthorizationFunction =POST=> Client Backend =POST Response=> Client App TPSAuthorizationFunction

The TPS API would need to return back a composite signature data structure containing:
(See: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md#how-to-create-an-authorization-function for more information on the purpose of each field)
{
addr: tpsSignerAddress
keyId: tpsSignerKeyID
signature: tpsSignature
}
**/
return await fetch(signingURL, {
method: "POST",
headers: { "Content-Type": "application/json" }
body: JSON.stringify(signable)
}).then(res => res.json())
}
}
}
```

The application would need to expose an API/webservice with POST routes for the
`resolveAccountURL` and `signingURL` URLs the `TPSAuthorizationFunction`
would use. The application would need to perform the actions of resolving the account,
or signing the signable for those routes respectively.

The Client Application's backend might look something like:

```javascript
// IN CLIENT APPLICATION BACKEND
import { TPSAccountResolver, TPSSigner } from "@TPS/TPS-server"

var express = require('express')
var app = express()

const TPSConfiguration = {
accounts: [
{
address: "0xABC123",
privateKey: process.env.ACCOUNT_ABC123_PRIVATE_KEY_HEX,
keyIndex: 0
},
{
address: "0x456DEF",
privateKey: process.env.ACCOUNT_456DEF_PRIVATE_KEY_HEX,
keyIndex: 0
}
],
}

const configuredTPSAccountResolver = await TPSAccountResolver(TPSConfiguration)
const configuredTPSSigner = await TPSSigner(TPSConfiguration)

app.post("/tps/resolveaccount", (req, res) => {
const account = req.body

const resolvedAccount = await configuredTPSAccountResolver(account)

res.send(resolvedAccount)
})

app.post("/tps/sign", (req, res) => {
const signable = req.body

const compositeSignature = await configuredTPSSigner(signable)

res.send(compositeSignature)
})
```

The applications backend might consume a package, `"@TPS/TPS-server"`, which would ideally expose some helpful utilities
for handling the logic behind resolving an account and producing a payer signature. The above code snippet uses these
potential utilities.

The implementation of these utilities, `TPSAccountResolver` and `TPSSigner` might look like:

```javascript
// IN @TPS/TPS-server

import {WalletUtils} from "@onflow/fcl"

export const TPSAccountResolver = ({ accounts }) => (account) => {

// Select an available account address from those provided in the configuration.
const tpsPayerAddress = selectAvailableAddress(accounts)

// Select the Key ID of thekey on the Payer account that will be used during signing.
const tpsPayerKeyID = selectKeyID(tpsPayerAddress, accounts)

// Produce a unique identifier for this payer address and key id combination.
const tpsTempID = `${tpsPayerAddress}-${tpsPayerKeyID}`

return ({
...account,
tempId: tpsTempID
addr: tpsPayerAddress
keyId: tpsPayerKeyID
})
}

export const TPSSigner = ({ accounts }) => (signable) => {
// Get the address specified in the signable as the payer.
const signablePayerAddress = signable.voucher.payer

// Get the account specified in the TPSSigner configuration that corresponds
// to the signablePayerAdress.
const account = accounts.find(a => a.address === signablePayerAddress)

// Throw an error if the signablePayerAddress is not found in the TPSSigner configuration
if (!account) {
throw new Error(`TPSSigner Error: Could not find account for signablePayerAddress=${signablePayerAddress}`)
}

// For security, ensure that `signablePayerAddress` is not specified as a transaction authorizer or proposer.
if (signable.voucher.authorizers.includes(signablePayerAddress)) {
throw new Error(
`TPSSigner Error: signablePayerAddress=${signablePayerAddress} specified as a transaction authorizer in transaction signable.`
)
}

if (signable.voucher.proposalKey.address === signablePayerAddress) {
throw new Error(
`TPSSigner Error: signablePayerAddress=${signablePayerAddress} specified as the transaction proposer in transaction signable.`
)
}

// Encode the signable using WalletUtils
const encodedMessage = WalletUtils.encodeMessageFromSignable(signable, signablePayerAddress)

// Prodce a signature, as a hex string
const signature = toHexString(YourFavouriteCryptoUtility.sign(encodedMessage, account.privateKey))

return signature
}
```

The user of the TPS utility would be responsible for maintaining that the accounts they specify into `TPSAccountResolver` and `TPSSigner` as configuration have a sufficient FLOW balance at all times to pay for transactions they sign for.

> To re-iterate, this is just an example implementation of what _might_ work. Your
implementation may vary extensively.

### Drawbacks

Since the TPS would be responsible for paying for transactions, it should have suffient
security mechanisms in place to safeguard itself.

Since tranaction fees can be dynamic, increasing depending on the amount of work the network
would need to perform for each transaction, the payer service should be smart enough to take
this into consideration as needed.

### Dependencies

The Transaction Fee Payer Service would depend on FCL. Should there be changes to any aspect
of FCL that may impact the TPS, the service would have to be updated accordingly.

### Engineering Impact

This is a rather large project. While an MVP could be built by an engineering team, a full solution
would likely require multiple participants and skillsets.

### Compatibility

This project would need to maintain compatibility with latest versions of FCL.

## Questions and Discussion Topics

- Are the other approches to desiging a TPS that differ from the example provided in this FLIP?