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

Add ERC-7798: Tap to Pay #686

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Changes from 4 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
342 changes: 342 additions & 0 deletions ERCS/erc-_____.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
---
eip: ____
amhed marked this conversation as resolved.
Show resolved Hide resolved
title: Contactless Payment
description: Standard for initializing contactless payment transactions from EVM wallets
author: Amhed Herrera (@amhed), Justin Lee (@JustinDLee), Arjun Dureja (@arjun-dureja)
discussions-to:
amhed marked this conversation as resolved.
Show resolved Hide resolved
status: Draft
type: Standards Track
category: ERC
created: 2024-10-25
requires: 20, 681, 712
---

## Abstract
This ERC defines a standard for contactless payment transactions that can allow for interoperable customer to merchant onchain payments, regardless of the wallets the customer and merchant are using.

## Motivation
Currently there is no standard mechanism in crypto to do contactless payment transactions via NFC. This ERC defines a standardized way of exchanging payment information between merchants and customers, or peer-to-peer individuals, so that efficient checkout can occur.

## Specification

Comprised of three parts:
- A new Ethereum Provider JavaScript API method called `requestContactlessPayment`
- An agreement on the payload for data exchange between the parties
- An optional mechanism for relaying large JSON payloads using a backend relayer


### Use Cases

#### Use Case 1: Customer purchases from a merchant
1. A customer approaches a merchant and orders an item at their cash register (e.g. a cup of coffee).
1. The merchant is running an app on their point-of-sale (PoS) system that allows initiating a checkout flow.
1. When the customer is done ordering, the merchant goes through the checkout flow up and triggers a payment request.
1. The merchant app calls the `requestContactlessPayment` RPC method with the expected payload.
1. The merchant's device emits an NFC signal via host card emulation ([HCE](https://developer.android.com/develop/connectivity/nfc/hce)) for the customer’s device to read.

Choose a reason for hiding this comment

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

Is this done via the Ethereum provider receiving the above call? So is it assumed the provider needs to be running on device?

Copy link
Author

Choose a reason for hiding this comment

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

Yes

1. The customer, running their wallet software of choice, reads the NFC signal. The customer’s wallet constructs the necessary transaction(s) to execute the onchain checkout.

Choose a reason for hiding this comment

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

Sorry I don't know much about this, why couldn't we just do a normal eth_sendTransaction or wallet_sendCalls via an NFC channel?

Copy link
Author

Choose a reason for hiding this comment

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

We had some sluggish performance transmitting the NFC message with larger payloads and decided to use a relayer for a shorter URI.

Copy link
Contributor

Choose a reason for hiding this comment

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

you could still use a relayer by using wallet_sendCalls + something like a nfcRelay capability

1. The wallet then signs and submits all transactions.
1. Upon successful onchain execution of these transactions, the checkout is complete.

#### Use Case 2: Peer-to-peer Sends
1. Alice requests a payment on her wallet app by emitting an NFC signal. The contents of the signal is an EIP-681 URI

Choose a reason for hiding this comment

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

I think 681 is possibly over specified on things like gas values. Wonder if we could align on something more minimal.

Copy link
Author

Choose a reason for hiding this comment

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

We're already using 681 heavily on send/receive flows on CB Wallet. We tried leaning into existing standards. What would you suggest could be better?

> []will this only work with 681? 681 has limitations, like receiver intent which would be great to solve too.

EIP-681 URI1. Bob reads the NFC signal from Alice on his wallet app and constructs the transaction
2. Bob holds his phone close to Alice's which triggers the NFC flow.
1. Bob then signs and submits the transaction onchain.
1. Send flow is complete.

### requestContactlessPayment
The requestContactlessPayment method will be added as a standard method to the [EIP-1193 Ethereum Provider JavaScript API](https://eips.ethereum.org/EIPS/eip-1193).

This method takes in two parameters: `type` and `uri`.

- `type` is an enum that represents the payload type:
- 1 means the URI is an EIP-681-compliant payload.
- 2 means the URI points to an HTTP relayer, where the response of the relayer is either a JSON object with the transaction data already encoded as a 0x string, or a message for the customer to sign. The customer wallet is responsible for constructing, signing and submitting the transaction/message.

- `uri` represents either the EIP-681-compliant payload or the relayer endpoint to query for the JSON payload.

Choose a reason for hiding this comment

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

I feel like 681 URI is not widely adopted and is mostly useful when you can share a string with someone and not a whole payload.

But here we control the channel so I'm again wondering why not just use existing wallet RPCs to describe what we want.

Copy link
Author

Choose a reason for hiding this comment

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

You're right, it's not widely adopted. Open to any other suggestions 🙏


Example calls:

```js
window.ethereum.request({
method: 'requestContactlessPayment',
params: {
type: 1,
uri: "ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0xf7d07Ee99095FDC09001710Ec232162a788BB989&uint256=1e5",
});

window.ethereum.request({
method: 'requestContactlessPayment',
params: {
type: 2,
uri: "https://nfcrelay.xyz/paymentTxParams?uuid=1234-abcd-56678",
verificationCode: "1234567890"
}
});
```

### Type 1

The transaction is a regular crypto send. The receiving party transmits the EIP-681 URI to the sender’s wallet.

Example code using an EIP-681 payload:

```ts
import { ethers } from 'ethers';

const provider = new ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_KEY");

function handleRequestContactlessPaymentEIP681URI(url: string) {
// Parse the EIP-681 URL
const params = url.split('@')[1].split('?')[-1].split('&');
const to = params[0].split('=')[1];
const value = parseInt(params[1].split('=')[1]);
const chainId = url.split('@')[0];

// Get wallet and connect to provider
const wallet = new ethers.Wallet(privateKey, provider);

// Build and sign transactions
const tx = {
to: to,
value: ethers.utils.parseEther(value.toString()),
gasLimit: 21000,
gasPrice: ethers.utils.parseUnits('50', 'gwei')
};

const signedTx = await wallet.signTransaction(tx);

// Send transaction
return await provider.sendTransaction({ signedTransaction: signedTx, chainId });
}
```

### Type 2

Handles cases where the payload necessary to construct, sign, and submit the transaction needed for payment is too large to emit over NFC. The intended flow is as follows:

1. The merchant dapp makes a POST request to an NFC relayer endpoint (See [example implementation](https://github.com/base-org/nfc-relayer)) with the information needed for completing a sale.
1. The merchant’s device then transmits the NFC relayer endpoint URI and verification code to the customer via any means
1. The customer’s device then makes a GET request to the endpoint URI

One of three payloads can be returned from the NFC relayer:

**Regular Send**
```ts
{
payloadType: 'eip681';
chainId: string;
contractAddress: string;
toAddress: string;
value: string;
dappUrl?: string;
dappName?: string;
rpcProxySubmissionParams?: {
submissionUrl?: string;
}
}
```

**Contract Call**

```ts
{
payloadType: 'contractCall';
chainId: string;
approveTxs?: {
data: string; // this represents the approval call data
toAddress: string; // the address to submit the approve transaction to
}[],
paymentTx: {
data: string; // call data of the transaction, only needed for
toAddress: string; // the address to submit the payment transaction to
value: string; // the value of the blockchain's native token to transfer over
},
rpcProxySubmissionParams?: {
submissionUrl?: string; // optional endpoint to submit the tx hash to
},
dappUrl?: string;
dappName?: string;
}
```

**[EIP-712 Message](https://eips.ethereum.org/EIPS/eip-712) (offline signature)**

```ts
{
payloadType: 'eip712';
chainId: string,
rpcProxySubmissionParams: {
submissionUrl: string; // endpoint to submit tx message + signature to
typedData: {
types: {
// any types can go here in accordance with EIP-712 and eth_signtypeddata_v4, it's recommended that every type needed for
// generating the right transactions on the RPC proxy is here
},
primaryType?: string;
domain: {
// any domain fields go here, usually need name, version, chainId and verifyingContract
},
message: {
// any fields can go here, it's recommended that every field needed for
// generating the right transactions on the RPC proxy is here
}
},
},
dappUrl?: string;
dappName?: string;
additionalPayload?: {
// ... any fields can go here, this is additional payload the RPC proxy may need but does not have to bee signed in the message
}
}
```
Upon submitting the transaction for the EIP-681 and contract call cases, if `rpcProxySubmissionParams` is present, then the transaction hash can be optionally submitted via a POST request to the submission URL. The body of the POST would have the following structure:
```ts
{
txHash: string;
}
```

For EIP-712 messages, the signature is to be submitted alongside the typed data message to the provided `submissionUrl`. The body of the POST request would look something like this:


```ts
{
typedData: {
types?: {
// define types here
},
primaryType?: string;
domain?: {
name?: string;
version?: string;
chainId?: string;
verifyingContract?: string;
},
message?: {
// define fields of the message needed to be signed here
},
},
signature: string; // EIP-712 signature should be here
additionalPayload?: {
// ... any fields can go here, this is additional payload the RPC proxy may need but does not have to be signed in the message
}
}
```

The RPC proxy URL is then expected to return the tx hash in a 200 response for both cases.

#### Example code for this scenario

##### EIP-681

```ts
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(INSERT_PRIVATE_KEY_HERE, INSERT_PROVIDER_HERE);

function handleRequestContactlessPaymentTxDataPayload(uri: string) {
const relayedData = await fetch(uri).json();

if (relayedData.payloadType !== 'eip681') return;

const isERC20Transfer = !!relayedData.contractAddress;

const paymentTx = {
to: relayedData.toAddress,
value: isERC20Transfer ? 0 : relayedData.value,
data: isERC20Transfer ? encodeERC20Transfer(relayedData.toAddress, relayedData.value) : undefined, // pseudocode for actually encoding an ERC20 transfer here
gasLimit: 21000,
gasPrice: ethers.utils.parseUnits('50', 'gwei')
};

// insert code here to show the transactions to the user
// and allow them to sign and submit them

const txHash = await wallet.signTransaction(paymentTx);
return fetch(relayerData.rpcProxySubmissionParams.submissionUrl, {
method: 'POST',
'Content-Type': 'application/json',
body: JSON.stringify({ txHash });
});
}
```

##### Approve + payment transactions

```ts
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(INSERT_PRIVATE_KEY_HERE, INSERT_PROVIDER_HERE);

function handleRequestContactlessPaymentTxDataPayload(uri: string) {
const relayedData = await fetch(uri).json();

if (relayedData.payloadType !== 'contractCall') return;

const approveTxs = relayedData.approveTxs.map((approveTx) => {
return {
to: approveTx.toAddress,
data: approveTx.data
};
});
const paymentTx = {
to: relayedData.paymentTx.toAddress,
data: relayedData.paymentTx.data,
value: relayedData.paymentTx.value,
gasLimit: 21000,
gasPrice: ethers.utils.parseUnits('50', 'gwei')
};

// insert code here to show the transactions to the user
// and allow them to sign and submit them

const signedApproveTxs = await Promise.all(approveTxs.map((approveTx) => {
return wallet.signTransaction(approveTx);
}));
const signedTx = await wallet.signTransaction(paymentTx);
return fetch(relayerData.rpcProxySubmissionParams.submissionUrl, {
method: 'POST',
'Content-Type': 'application/json',
body: JSON.stringify({ txHash: signedTx });
});
}
```

##### EIP-712
```ts
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(INSERT_PRIVATE_KEY_HERE, INSERT_PROVIDER_HERE);

function handleRequestContactlessPaymentTxMessagePayload(uri: string) {
const relayedData = await fetch(uri).json();

if (relayedData.payloadType !== 'eip712') return;

const message = relayedData.message;

// insert code here to show the message to the user
// and allow them to sign it

const signature = await wallet._signTypedData(message.domain, message.types, message.message);

return fetch(relayerData.rpcProxySubmissionParams.submissionUrl, {
method: 'POST',
'Content-Type': 'application/json',
body: JSON.stringify({ message: relayedData.message, signature, additionalPayload: relayedData.additionalPayload });
});
}
```


## Rationale

The reason for having a relayer URI to pass data is due to limitations in the size of the data that can be transmitted wirelessly between devices directly. For example, QR codes have a maximum character size of 7,089 characters currently, but a user can attempt to buy an indefinite number of items in a single checkout transaction. By having a relayer URI, we can theoretically pass as much calldata as we desire to the customer’s wallet, as long as they fetch from the URI.

EIP-681 URIs were also permitted to enable possible direct peer to peer payments. It’s more likely that they’ll be occurring between two end user wallets rather than between a customer and a merchant, but it would be good to include in the event the dapp only requires a simple send transaction to execute the checkout.

## Security Considerations
In the case of an EIP-681 URI, the same security considerations apply here. The wallet should display the amount and asset being transferred as well as the recipient very clearly to the customer, and users should only execute transactions using EIP-681 URIs from trusted dapps.

With regards to the relayer URI, since it is a publicly facing URI, it’s possible that anyone can make a fetch to it, grab the data, and submit transactions of their own to execute a checkout on someone else’s behalf. To mitigate this, we made a verification code as an optional part of the standard. The verification code is only passed through via the RPC call and is expected to be passed onto the customer’s wallet via some sort of contactless communication mechanism. This way, the data is not accessible to a random person on the internet and can only be accessed from the intended customer’s wallet.

Loading