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

CIP-0030 | Dapp-Connector #88

Merged
merged 15 commits into from
Nov 17, 2021
Merged
Changes from 6 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
236 changes: 236 additions & 0 deletions CIP-DappConnector/CIP-dapp-connector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
---
CIP: ?
crptmppt marked this conversation as resolved.
Show resolved Hide resolved
Title: Cardano dApp-Wallet Web Bridge
Authors: rooooooooob
Copy link
Member

@KtorZ KtorZ Nov 9, 2021

Choose a reason for hiding this comment

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

Possibly, add more authors here to include people who have actively contributed to the writing of this specification?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What would we use for the threshold? Like @alessandrokonrad who has contributed significantly to discussion?

Status: Draft
Type: Standards
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved
Created: 2021-04-29
License: CC-BY-4.0
---

# Abstract

This documents describes a webpage-based communication bridge allowing webpages (i.e. dApps) to interface with Cardano wallets. This is done via injected javascript code into webpages. This specification defines the manner that such code is to be accessed by the webpage/dApp, as well as defining the API for dApps to communicate with the user's wallet. This document currently concerns the Shelley-Mary era but will have a second version once Plutus is supported. This specification is intended to cover similar use cases as web3 for Ethereum or [EIP-0012](https://github.com/ergoplatform/eips/pull/23) for Ergo. The design of this spec was based on the latter.
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved


# Motivation

In order to facilitate future dApp development, we will need a way for dApps to communicate with the user's wallet. While Cardano does not yet support smart contracts, there are still various use cases for this, such as NFT management. This will also lay the groundwork for an updated version of the spec once the Alonzo hardfork is released which can extend it to allow for Plutus support.



# Specification

## Version

The API specified in this document will count as version 0.1.0 for version-checking purposes below.

## Data Types

### Bytes

A hex-encoded string of the corresponding bytes.

### cbor\<T>

A hex-encoded string representing [CBOR](https://tools.ietf.org/html/rfc7049) corresponding to `T` defined via [CDDL](https://tools.ietf.org/html/rfc8610) either inside of the [Shelley Mary binary spec](https://github.com/input-output-hk/cardano-ledger-specs/blob/0738804155245062f05e2f355fadd1d16f04cd56/shelley-ma/shelley-ma-test/cddl-files/shelley-ma.cddl) or, if not present there, from the [CIP-0008 signing spec](https://github.com/cardano-foundation/CIPs/blob/master/CIP-0008/CIP-0008.md).
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved
This representation was chosen when possible as it is consistent across the Cardano ecosystem and widely used by other tools, such as [cardano-serialization-lib](https://github.com/Emurgo/cardano-serialization-lib), which has support to encode every type in the binary spec as CBOR bytes.

### TransactionUnspentOutput

If we have CBOR specified by the following CDDL referencing the Shelley-Mara CDDL:
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved
```cddl
transaction_unspent_output = [
input: transaction_input,
output: transaction_output,
]
```
then we define
```
type TransactionUnspentOutput = cbor<transaction_unspent_output>
```

This allows us to use the output for constructing new transactions using it as an output as the `transaction_output` in the Shelley Mary CDDL does not contain enough information on its own to spend it.
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved

### Paginate

```
type Paginate = {|
page: number,
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't sufficient information because wallet state could change in-between pages (either new entries added or a rollback causing entries to be removed)
For the backend, instead of using arbitrary pages we instead give a concrete value like "all values after this address"

Due to this fact, I don't think it necessarily makes sense to have a unified paging system since the anchor to use depends on the data type you're looking for (txs, addresses, etc.)

If you feel this problem isn't really solvable, probably we should at least mention the pagination system has this possibility for error

limit: number,
|};
```
Used to specify optional pagination for some API calls. Limits results to {limit} each page, and uses a 0-indexing {page} to refer to which of those pages of {limit} items each.


## Error Types

### APIError

```
APIErrorCode {
InvalidRequest: -1,
InternalError: -2,
Refused: -3,
}
APIError {
code: APIErrorCode,
info: string
}
```

* InvalidRequest - Inputs do not conform to this spec or are otherwise invalid.
* InternalError - An error occurred during execution of this API call.
* Refused - The request was refused due to lack of access - e.g. wallet disconnects.

### DataSignError

```
DataSignErrorCode {
ProofGeneration: 1,
AddressNotPK: 2,
UserDeclined: 3,
InvalidFormat: 4,
}
type DataSignError = {
code: DataSignErrorCode,
info: String
}
```

* ProofGeneration - Wallet could not sign the data (e.g. does not have the secret key associated with the address)
* AddressNotPK - Address was not a P2PK address and thus had no SK associated with it.
* UserDeclined - User declined to sign the data
* InvalidFormat - If a wallet enforces data format requirements, this error signifies that the data did not conform to valid formats.

### PaginateError

```
type PaginateError = {|
maxSize: number,
|};
```
{maxSize} is the maximum size for pagination and if the dApp tries to request pages outside of this boundary this error is thrown.

### TxSendError

```
TxSendErrorCode = {
Refused: 1,
Failure: 2,
}
type TxSendError = {
code: TxSendErrorCode,
info: String
}
```

* Refused - Wallet refuses to send the tx (could be rate limiting)
* Failure - Wallet could not send the tx

### TxSignError

```
TxSignErrorCode = {
ProofGeneration: 1,
UserDeclined: 2,
}
type TxSignError = {
code: TxSignErrorCode,
info: String
}
```

* ProofGeneration - User has accepted the transaction sign, but the wallet was unable to sign the transaction (e.g. not having some of the private keys)
* UserDeclined - User declined to sign the transaction



## Initial API

In order to initiate communication from webpages to a user's Cardano wallet, the wallet must provide the following javascript API to the webpage. A shared, namespaced `cardano` object must be injected into the page if it did not exist already. Each wallet implementing this standard must then create a field in this object with a name unique to each wallet containing a `wallet` object with the following methods. The API is split into two stages to maintain the user's privacy, as the user will have to consent to `cardano.walletName.requestReadAccess()` in order for the dApp to read any information pertaining to the user's wallet.

### wallet.requestReadAccess(): Promise\<API>
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Member

Choose a reason for hiding this comment

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

What about using a dedicated namespace on the global object instead of prefixed functions. That is, have Window.Cardano.request_read_access etc ... ?

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm. A cardano object is mentioned below; does it mean that we expect these two next functions to be available globally and to create the global cardano reference as a result 🤔 ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Exactly. The cardano object is first of all injected if the cardano_requests_read_access function returns true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We did this two-phase so that the cardano object was separate and if it exists then it meant that the rest of the API was injected. It was originally all in there e.g. cardano.request_read_access() but we changed the decision later. However, if the user disconnects the wallet then dApps still need to handle cardano.*() returning an error that the wallet is not connected, so it's possible we could revert to that behavior.

At page load those 2 functions are injected, and if cardano_request_read_access() is successful then the cardano object is injected with the rest of the API as alessandro mentioned.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another idea I had thought of previously was if cardano_request_read_access() returns a Cardano instance instead of injecting it as global.cardano. We would still need to inject the type definition if it's not injected at wallet start, and if it is at wallet start we would want to control in the implementation to not allow dApps creating their own object from that type and trying to use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd be curious to know why, to me it seems more consistent to keep everything under the same namespace.

Well at first I had wanted to simplify things so that dApps wouldn't have access to those APIs until they had been granted access without having to check for API access denied errors in every endpoint, but it's not really a valid point once you realize that you'll need to handle those anyway if a wallet cancels access later on after injection. It does let us potentially go with that returning the cardano object idea instead of performing a 2nd injection so that was a consideration as well.

Copy link
Contributor

@alessandrokonrad alessandrokonrad Jun 22, 2021

Choose a reason for hiding this comment

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

Yeah I think just hiding the endpoints, doesn't fully prevent someone from accessing the wallet actually, since you communicate with window.postMessage. so the prevention needs to be in the extension itself. I would also prefer to move it under the same name space. and maybe make it shorter like:
cardano.enable and cardano.is_enabled

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alessandrokonrad It absolutely doesn't on its own. That is up to the wallet's implementation. When we implemented the ergo dApp connector spec in Yoroi we handle the connections from inside the Yoroi extension so Yoroi will only only respond to requests from pages that it considers connected, which can only happen if the connect request was accepted by the user (either by whitelisting the site or by the popup). You can't assume much at all in the way of security guarantees whatsoever in the injected code. That's why we treat the yoroi-ergo-connector (especially the injected code) as a trustless relay, and inside of yoroi-frontend we verify the inputs and keep track of connections.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Concerning this, we could potentially expand on this and other implementation-related security issues in the spec, but it might be outside of the scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed how these things work while I was reworking the wallet connection UX for when you have multiple wallets (e.g. Yoroi + AdaLite) that need to inject their API.

Errors: APIError

This is the entrypoint to start communication with the user's wallet. The wallet should request the user's permission to connect the web page to the user's wallet, and if permission has been granted, the full API will be returned to the dApp to use. The wallet can choose to maintain a whitelist to not necessarily ask the user's permission every time access is requested, but this behavior is up to the wallet and should be transparent to web pages using this API. If a wallet is already connected this function should not request access a second time, and instead just return the `API` object.

### wallet.checkReadAccess(): Promise\<bool>
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved

Errors: APIError

Returns true if the dApp is already connected to the user's wallet, or if requesting access would return true without user confirmation (e.g. the dApp is whitelisted), and false otherwise. If this function returns true, then any subsequent calls to `wallet.requestReadAccess()` during the current session should succeed and return the `API` object.

### wallet.version(): String
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved

Errors: APIError

Returns the version number of the API that the wallet supports.


### wallet.name(): String
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we want to return a base64url (or whatever) encoded image for an icon for the wallet too?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes an image would be great. Maybe as URI, so base64 and http/https links.

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
### wallet.name(): String
### wallet.name: String

https://github.com/cardano-foundation/CIPs/pull/88/files#r682429605
Can be implemented as a getter


Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion:

Change wallet.requestReadAccess() -> wallet.enable()
wallet.checkReadAccess() -> wallet.isEnabled()

I like to keep things simple and short and it would also align more with the ethereum API.
Maybe a wallet.disable() function would make sense as well (simply removing from whitelist)

What about having wallet.version(), wallet.name() as properties directly instead of funtions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Part of the reason why they had long names before was so we wouldn't inject code that would conflict with existing names, but now that it's something like cardano.walletNameHere.requestReadAccess() that sounds like a good idea to simplify it to just cardano.walletNamehere.enable().

What about having wallet.version(), wallet.name() as properties directly instead of funtions?

That could work too.

Maybe a wallet.disable() function would make sense as well (simply removing from whitelist)

Are we imagining that wallet.disable() method to be for like a "sign out" button in the dApps? The way a whitelist is handled in the spec is left up to the wallet if and how they want to implement it, so whether this endpoint would simply end the session or remove seems like it would also be left up to the wallet. So I think the only guarantee we could make on this is that it disconnects the wallet from the page - subsequent calls to enable() could still reconnect you without user interaction if the wallet implements a whitelist and the site is whitelisted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I think the enable/disable functionality only really makes sense if the wallet has a whitelist, otherwise you don't really connect and disconnect and it's just a UI thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Even without a whitelist it still makes some sense. You could have it simply disconnect the session between the dApp and the wallet, meaning no more access to the api endpoints until the dApp requests to connect again. This could be relevant for privacy-focused wallets/users who would like to control when the dApp can query their wallet I guess. Since the way i see it there are 3 possible connect/whitelist situations:

  1. The whitelist is combined with the concept of being connected. This is what MetaMask and Yoroi's Ergo dApp connector use. To connect you need to effectively add the site to a whitelist, and then even if the user closes the browser/etc, the next time the dApp requests access it connects automatically. This is basically long-term session connections.
  2. The whitelist exists and is separate from the concept of being connected. We had this as our initial design for the Ergo connector but switched to 1. for UX simplicity for the average user. So the whitelist just meant that calls to wallet.requestReadAccess() would resolve successfully without user input, and you could disconnect from a site without removing it from the whitelist.
  3. There is no whitelist at all. You would have to accept connect requests every single website session.

Errors: APIError

Returns a name for the wallet which can be used inside of the dApp for the purpose of asking the user which wallet they would like connect with.



## Full API

Upon successful connection via `wallet.requestReadAccess()`, a javascript object we will refer to as `API` (type) / `api` (instance) is returned to the dApp with the following methods. All read-only methods (all but the signing functionality) should not require any user interaction as the user has already consented to the dApp reading information about the wallet's state when they agreed to `wallet.requestReadAccess()`. The remaining methods `api.signTx()` and `api.signData()` must request the user's consent in an informative way for each and every API call in order to maintain security.

The API chosen here is for the minimum API necessary for dApp <-> Wallet interactions without convenience functions that don't strictly need the wallet's state to work. The API here is for now also only designed for Shelley's Mary hardfork and thus has NFT support. When Alonzo is released with Plutus support this API will have to be extended.

### api.getUtxos(amount: cbor\<value> = undefined, paginate: Paginate = undefined): Promise\<TransactionUnspentOutput[] | undefined>

Errors: `APIError`, `PaginateError`

If `amount` is `undefined`, this shall return a list of all UTXOs (unspent transaction outputs) controlled by the wallet. If `amount` is not `undefined`, this request shall be limited to just the UTXOs that are required to reach the combined ADA/multiasset value target specified in `amount`, and if this cannot be attained, `undefined` shall be returned. The results can be further paginated by `paginate` if it is not `undefined`.

### api.getBalance(): Promise\<cbor\<value>>

Errors: `APIError`

Returns the total balance available of the wallet. This is the same as summing the results of `api.getUtxos()`, but it is both useful to dApps and likely already maintained by the implementing wallet in a more efficient manner so it has been included in the API as well.

### api.getUsedAddresses(paginate: Paginate = undefined): Promise\<cbor\<address>[]>

Errors: `APIError`

Returns a list of all used (included in some on-chain transaction) addresses controlled by the wallet. The results can be further paginated by `paginate` if it is not `undefined`.

### api.getUnusedAddresses(): Promise\<cbor\<address>[]>

Errors: `APIError`

Returns a list of unused addresses controlled by the wallet.

### api.getChangeAddress(): Promise\<cbor\<address>>

Errors: `APIError`

Returns an address owned by the wallet that should be used as a change address to return leftover assets during transaction creation back to the connected wallet. This can be used as a generic receive address as well.

Copy link
Contributor

@refi93 refi93 Jul 12, 2021

Choose a reason for hiding this comment

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

What call would the dApp need to invoke to fetch the staking key of the wallet, e.g. to perform a stake delegation? It could infer it from the addresses returned via any of the already existing get_*_address() call but that seems hacky, so I'd consider adding a get_stake_address() call too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added in a api.getRewardAddresses() endpoint.

### api.signTx(tx: cbor\<transaction>, partialSign: bool = false): Promise\<cbor\<transaction_witness_set>>

Errors: `APIError`, `TxSignError`

Requests that a user sign the unsigned portions of the supplied transaction. The wallet should ask the user for permission, and if given, try to sign the supplied body and return a signed transaction. If `partialSign` is true, the wallet only tries to sign what it can. If `partialSign` is false and the wallet could not sign the entire transaction, `TxSignError` shall be returned with the `ProofGeneration` code. Likewise if the user declined in either case it shall return the `UserDeclined` code. Only the portions of the witness set that were signed as a result of this call are returned to encourage dApps to verify the contents returned by this endpoint while building the final transaction.

### api.signData(addr: cbor\<address>, sigStructure: cbor\<Sig_structure>): Promise\<Bytes>

Errors: `APIError`, `DataSignError`

This endpoint utilizes the [CIP-0008 signing spec](https://github.com/cardano-foundation/CIPs/blob/master/CIP-0008/CIP-0008.md) for standardization/safety reasons. It allows the dApp to request the user to sign data conforming to said spec. The user's consent should be requested and the details of `sig_structure` shown to them in an informative way. The Please refer to the CIP-0008 spec for details on how to construct the sig structure.

### api.submitTx(tx: cbor\<transaction>): Promise\<hash32>

Errors: `APIError`, `TxSendError`

As wallets should already have this ability, we allow dApps to request that a transaction be sent through it. If the wallet accepts the transaction and tries to send it, it shall return the transaction id for the dApp to track. The wallet is free to return the `TxSendError` with code `Refused` if they do not wish to send it, or `Failure` if there was an error in sending it (e.g. preliminary checks failed on signatures).

rooooooooob marked this conversation as resolved.
Show resolved Hide resolved
# Implementations

TODO: link to Yoroi's implementation once available