dfinity / ICRC

Repository to ICRC proposals
Apache License 2.0
28 stars 5 forks source link

ICRC-22: Payment request formats #22

Open peterpeterparker opened 1 year ago

peterpeterparker commented 1 year ago

ICRC-22 - Payment request formats

Data Details

Context

A standard way of representing payment requests as URLs, which is particularly useful for generating QR codes.

Abstract

"URLs embedded in QR-codes, hyperlinks in web-pages, emails or chat messages provide for robust cross-application signaling between very loosely coupled applications. A standardized URL format for payment requests allows for instant invocation of the user's preferred wallet application (even if it is a webapp or a swarm đapp), with the correct parameterization of the payment transaction only to be confirmed by the (authenticated) user." - source EIP-681.

References

Introduction

The following is a simple adaptation of above listed references to establish a standard way of sharing URLs that represent payment requests on the Internet Computer.

Specification

Payment request URLs are constructed by incorporating the ICRC "token" in the schema (protocol) part of the URL. The format for creating such URLs is as follows:

urn           = token ":" address [ "?" params]
token         = string
address       = <principal>-<checksum>.<compressed-subaccount>
params        = param [ "&" params ]
param         = [ amountparam ]
amountparam   = "amount=" *digit [ "." *digit ]

Where token epresents the symbol of an ICRC token in lowercase, such as "icp" for the Internet Computer.

The address refers to a textual encoding representation of ICRC-1 accounts, as defined in the related standard.

The amount should be provided as a number. The amount represents the amount of tokens in the base unit used by the ledger. It is strongly recommended to use scientific notation for the amount. Decimal representation can be combined with scientific representation, e.g., 4.042E8 ICP means a count of 404200000 base units as managed by the ICP ledger.

domwoe commented 1 year ago

Thank you @peterpeterparker.

Did you consider using the canister id instead of the symbol to identify the token/currency?

peterpeterparker commented 1 year ago

Did you consider using the canister id instead of the symbol to identify the token/currency?

No, not really @domwoe. I feel like to support ICP and others non ICRC tokens (Bitcoin, ETH) as well, using the token/currency is the best approach in term of interpolarity and simplicity. But of course, only my two cents. The proposal is here to be discussed 😉.

atengberg commented 1 year ago

I misunderstood the address resolution, but seeing it first hand resolved it immediately; maybe I was thinking about forwarding. Anyways thanks for your patience.

Can we get a standardized set of encoded payments/and their QR codes while this is being decided for the sake of usability?

As to decimals, because ICRC1 token canisters can set their own decimal value, they should also be responsible for converting amounts they handle--the assumption shouldn't be the client will do that. I had setup a variant in the token canister to handle normal versus base unit amounts, though I didn't complete the module correctly (the dependency should have been inverted so the variant itself would apply the correct transforms) maybe this is something worth considering as part of this library?

Similarly on the client side, as they do in ether.js wrapping up a library for handling amounts conversions could be very helpful and prevent needless error. At least a standard BigNumber library would be a place to start.

peterpeterparker commented 1 year ago

Answering your question @atengberg but of course to be discussed, just giving context on the draft.

One thought is that unlike Bitcoin's network, as canisters' ids are a form of address themselves, then perhaps the intended recipient address is actually a "subaccount" of the canister's "address":

I described the address in the exact same way as it is defined in the ICRC-1 specification. See textual representation.

Although the question of security arises as whose responsible for validating the canister id that's used (in any case). And in any case, if the canister id is present (however it might be) then it can be used to validate the payment request (as opposed to the reliability of using the token symbol to resolve the canister id).

Indeed, I did not mention it. In other ICRCs, is there specific sections or limitations that address such issue or these also implies that the responsability is deferred?

For the amount, by number do you mean string representation of a number?

I meant number. 123456.789

roman-kashitsyn commented 11 months ago

I agree with @domwoe: token should be a canister ID. Otherwise, the payment request is not self-contained and requires an external token -> canisterID mapping.

Furthermore, it would be nice to handle more transaction types. For example, we probably want a way to represent icrc2_approve operations as a URI.

atengberg commented 11 months ago

Is not the principal of the address the token's canister id?

The one problematic case I've found is if decoding a payment, and the token is different than the expected token (from the canister id's query method of icrc1 metadata--whether it be from the payment encoding or known a priori), should the payment be considered a problem?

Or in code

// tokenSymbol is known beforehand derived from a icrc1_token_canister_metadata call
// useCallback is a React thing, hopefully clear enough this is part of a UI form. 

  const onQrCodeScanned = useCallback((result) => {
    try {
      const decoded = decodePayment(result);
      if (decoded?.token) {
        if (tokenSymbol && tokenSymbol !== decoded.token) { // HERE is the problem. 
          formik.setErrors({ 
            ...formik.errors,
            recipientAddressInput: 'Token symbol from QR code does not match token symbol from canister metadata'
          })
        }
      }
      if (decoded?.identifier) {
        formik.setFieldValue('recipientAddressInput', decoded.identifier);
      }
      if (decoded?.amount) {
        // Literalize as decoded payment will return 'number' type (conversion to base units handled on submit).
        formik.setFieldValue('amountInput', `${decoded.amount}`);
      }
      if (!decoded) {
        throw new Error("Nullish decoded, falling back to raw qr input result");
      }
   } catch (e) { }// Note if failed, as long as the result is not trivial it's simply set as the recipientAddress input. 

This is probably not that significant of a deal.

The only real other issue is that small numberish amounts can lose precision, which is why the EVM payment standards typically suggest using string to encode all amount values.

kristoferlund commented 10 months ago

Hey, I added ICRC-22 support to IC-POS.

https://github.com/kristoferlund/ic-pos

Screenshot 2023-09-10 at 22 54
dietersommer commented 8 months ago

Regarding the question of using the token symbol or canister id to identify the token, a unique id like the canister id is clearly preferable to disambiguate the situation of multiple tokens of the same name. However, @peterpeterparker mentions, he would like to have the option to also support tokens on other chains.

I feel like to support ICP and others non ICRC tokens (Bitcoin, ETH) as well, using the token/currency is the best approach in term of interpolarity and simplicity.

There is no reason why this should not be possible. Actually, the same problem applies to other chains: The smart contract address is the unique identifier for the token, not the symbol. There can be many ERC-20 tokens with the same name on an EVM chain.

Can't we use the smart contract id of the token on its respective blockchain and an additional chain identifier to solve this generically for multiple blockchains and tokens? The symbol is a convenience field that can be there as well to make it sensible to the user.

Now one question that comes up is how important it is to support other chains? There is a standard for Bitcoin and Ether and ERC-20 on Ethereum already. Would it be the goal to get adoption in the Ethereum world also by allowing ERC-20 payments to be encoded? Or is it sufficient to focus this only on the ICP network?

atengberg commented 8 months ago

I was certainly over-complicating the addressing format, and support for other chains in the textual representation is easily supported already as you point out.

Practically speaking, imho support for bitcoin and its direct derivatives would likely be enough. Theoretically speaking, support specifically for a plurality of interfaces is more inline with the nature of decentralized networking, and is likely going to remain one of the driving forces of web3 (one area where defi has still taken off). Consider ICP could be one of the few (if not only?) secure blockchain providers for BTC<>EVM tokenization, and more realistically could even be setting that tide to come if the case for ICP<>BTC is proven.

dietersommer commented 8 months ago

Update as of WG discussions

ICRC-22 — URI Format for Payment Requests

Information

See comments indicated with "// FIX" for open issues requiring further discussion.

// FIX Update introduction and motivation for this work.

Context

A standard way of representing payment requests as URLs, which is, for example, useful for generating QR codes to be read by wallets.

Abstract

"URLs embedded in QR-codes, hyperlinks in web-pages, emails or chat messages provide for robust cross-application signaling between very loosely coupled applications. A standardized URL format for payment requests allows for instant invocation of the user's preferred wallet application (even if it is a webapp or a swarm đapp), with the correct parameterization of the payment transaction only to be confirmed by the (authenticated) user." - source EIP-681.

References

Introduction

The following is an adaptation of above listed references to establish a standard way of sharing URLs that represent payment requests on the Internet Computer.

Specification

The below specification defines the syntax and (non-formal) semantics for an ICRC-22 payment request.

// FIX syntax details to be fixed once open questions have been answered.

request = network_identifier ":" contract_address "/" transaction [ "?" parameters ]
network_identifier = protocol ":"  network // network id
protocol = "icp" // always the same, means the current version of the protocol
network = "mainnet" // different networks, e.g., testnets or private deployments may be available in the future; hash of public key might be used here
contract_address = // canister address to which to make the call
transaction = // method on the canister to call
parameters = parameter [ "&" parameter ]
parameter = key "=" value
key = string
value = ...

The network_identifier uniquely identifies the network to make the transaction on. For ICP mainnet this is "icp:mainnet" (see ?? for the definition).

// FIX: we may want to use a truncated hash of the NNS public key to uniquely identify different networks in the future, resulting in "icp:" as identifier for ICP mainnet. Issue: What happens if the public key changes in the (very) unlikely event of an NNS subnet recovery being required with 1/2 < n < 2/3 nodes being recoverable, i.e., there is majority agreement on the state, but the high-threshold BLS signing key is not recoverable and a new key needs to be generated. The the recovered ICP mainnet would have a different identifier, even though it's in essence the same network with one technical detail, the NNS public key, changed.

The contract_address is the canister smart contract address of the contract the transaction is to be performed on. This is represented in the standardized format as described here. The address unambiguously determines the token that is to be transacted. // FIX: link

The transaction parameter specified the canister method to be called on the canister indicated through contract_address. This is the string-based name as it appears in the canister's Candid specification.

The parameters are the parameters of the method to be called. They are given in the order in which they appear in the Candid specification of the method to be called.

// FIX: Details need to be worked out to make it possible to call different methods we want to cover in the base standard (at least ICP and ICRC-1 transfer and ICRC-2 approve). The result may not be so far away from a generic standard covering the call of any method of canisters. Specifically: Should types be decomposed into their constituents or the compound values be provided using a specific or their Candid encoding?

Next, we exhaustively define the parameters required for realizing the supported transactions of the supported ledger standards:

The amount should be provided as a nonnegative integer number. The amount represents the amount of tokens in the base unit used by the ledger, i.e., 4 ICP tokens would amount to 4 * 10^8 = 400000000 base units as managed by the ICP token ledger. It is strongly recommended to use scientific notation for the amount. Decimal representation can be combined with scientific representation, e.g., 4.042E8 ICP means a count of 404200000 base units as managed by the ICP ledger. As only integer numbers are allowed as amount, the exponent MUST be greater than or equal the decimal places of the number in scientific representation.

// FIX How exactly can the mapping from the paramters to the Candid method signature be done? Is it sufficient to enumerate all named parameters, with the type inferable by the recipient using the canister's Candid specification which can be obtained from the canister?

// FIX How to handle optional fields in the Candid specification of the method? As long as the input can be constructed unambiguously, they can be left out, otherwise be included as key=null.

Examples

ICRC-1 token transfer

icp:mainnet:<icrc1_contract_address>/icrc1_transfer?from_subaccount=<subaccount>&to=<account>&amount=4.042E8&fee=<fee>&memo=<memo>&created_at_time=<timestamp>

ICP token transfer

icp:mainnet:<icp_contract_address>/transfer?from_subaccount=<subaccount>&to=<icp_address>&amount=4.042E8&fee=<fee>&memo=<memo>&created_at_time=<timestamp>

ICRC-2 approval

An ICRC-2 approval can be made on any ICRC-2-compliant ledger, e.g., the ICP ledger or any ICRC-1-compliant token ledger.

icp:mainnet:<icrc1_contract_address>/icrc2_approve?from_subaccount=<subaccount>&spender=<spender_account>&amount=<amount>&expected_allowance=<allowance>&...
dietersommer commented 8 months ago

We wanted to know the exact maximum length of our ICRC-1 textual encodings and wanted to know whether removing the dashes between the digit groups of the principal part of the representation would be sufficient for getting the textual representation of the account below the 128-byte limit imposed by the CAIP standard.

Let’s have a look at this:

The ICRC-1 textual representation is composed from the following components: principal “-“ checksum “.” subaccount where juxtaposition means concatenation. The maximum lengths of the individual components are: 63 1 7 1 64

Thus, the maximum overall length of an ICRC-1 textual encoding is 136, which is 8 characters too long.

The maximum-length principal contains 10 dashes. Removing either all but the one separating the principal from the checksum or all dashes both brings us again below the limit of 128 characters.

Thus, it would work to adopt this tweaked textual representation of the ICRC-1 textual encoding for representing an account as a query parameter and being CAIP compliant. It’s likely the simplest adaptation of the ICRC-1 textual representation we can do to get below the limit.

References:

dskloetd commented 5 months ago

Applications can register themselves as handlers for certain protocols. So an application might register itself as handling the icp protocol and then if you click on a icp: link, it will open that application to handle the URI.

So if we expect ICP wallets to handle most/all tokens on the IC, it makes sense to have a single protocol rather than a protocol per token where a wallet would need to register as a handler for every possible token on the IC.

But I've never seen URIs with multiple parts separated by multiple colons. Is this still a valid URI? Might it break protocol handlers as I described above? Why not put network/contract address/etc. in the params?