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

EIP-0012 - dApp-Wallet Web Bridge #23

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Changes from 3 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
324 changes: 324 additions & 0 deletions eip-0012.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
# EIP-0012: dApp-Wallet Web Bridge

* Authors: rooooooooob, Robert Kornacki
* Created: 10-Aug-2020
* Modified: 12-Mar-2021
* License: CC0
* Forking: not needed

## Description

This document describes a communication bridge between cryptocurrency wallets and Ergo dApps which interact via javascript code injected into a web context. The communication is initiated from the dApp to the wallet.

## Motivation

Distributed apps (dApps) often require access to a user's wallet in order to function, and this is typically from a web context. In other cryptocurrencies, such as Ethereum, this is done via a library such as web3 that injects code into a web browser that the dApp can interact with to create transactions or obtain address data using a user's wallet. This should be done in a manner that maximizes privacy and provides an easy user experience.

## API

The proposed API is limited to just the minimum wallet <-> dApp communication needed rather than providing lots of utility functions (tx building, data conversions, etc) or node-access (for UTXO scanning). This is different compared to web3 as that functionality could be modular in a separate library and Ergo smart contracts don't need as much direct node access for basic dApp functionality compared to Ethereum.

This API is accessible from a javascript object `ergo` that is injected into the web context upon wallet consent. Before this just the free `ergo_request_read_access()` and `ergo_check_read_access()` functions are injected into the web context. An event handler can also be registered to the web context to detect wallet disconnects tagged as `"ergo_wallet_disconnected"`.

Choose a reason for hiding this comment

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

In order to avoid conflicts with multiple wallets, can we do a slight change on the access API? Maybe something like ergo.{walletName}.requestReadAccess() and ergo.{walletName}.checkReadAccess()

Copy link
Author

Choose a reason for hiding this comment

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

Yeah that's what we ended up doing for CIP-0030 for Cardano which I based off of this. I'm not sure if it's fine to do such a breaking change at this point since this has been up for a long time although it's still a draft PR. I'll wait and see what the Ergo devs think about this. We also don't have any functionality for versioning either. I mentioned both of these in my comment above in August. There are additional events that can be of use for dApps as well that could be added.

Copy link
Member

Choose a reason for hiding this comment

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

Agree with @capt-nemo429 . Explicitly distinguishing wallets in the dApp connector API is a crucial feature. Shouldn't be too problematic to migrate.


The following definitions are provided using flow type annotations to describe the underlying javascript API / JSON data types.

### ergo_request_read_access(): Promise\<bool>

Errors: APIError

If the API was not already injected, requests access to the API, and if the user consents, injects the full API similar to EIP-1102 via an `ergo` object. Returns true if the API has been injected, or false if the user refused access.
Does nothing and returns true if the API was already injected.

The wallet can choose to maintain its own whitelist if desired but this is on the wallet side and not a part of this standard. It could possibly also expose a subset of its state as responses to the other calls but this is also out of scope. All following API methods are injected upon consent.

### ergo_check_read_access(): Promise\<bool>

Errors: APIError

Returns true if the full API was injected and we have read access, and false otherwise. This is potentially useful to have a cleaner UX on the dApp side (e.g. display request read access button for optional functionality rather than always requesting it).

### ergo.get_utxos(amount: Value = undefined, token_id: String = 'ERG', paginate: Paginate = 'undefined'): Promise\<Box[] | undefined>

Errors: APIError, PaginateError

Returns a list of all unspent boxes tracked by the wallet that it is capable of signing for.
Takes an optional {amount} parameter, if not `undefined`, limits the returned UTXOs to {amount} of the token {token_id} to create it. if {amount} is not `undefined` and there is not sufficient balance of the specified token to reach {amount} then `undefined` is returned.
If {paginate} is not `undefined` then results are returned in a paginated manner in chronological order of use on the blockchain. If {paginate} is out of range, `PaginateError` is returned.

### ergo.get_balance(token_id: String = 'ERG'): Promise\<Value>

Errors: APIError

Returns available balance of {token_id} owned by the wallet. A useful and minimal convenience functionality, that while could be served by iterating the return of `get_utxos` and summing, is useful enough and could be most likely quickly returned by wallets without needing to do this calculation.

### ergo.get_used_addresses(paginate: Paginate = 'undefined'): Promise\<Address[]>

Errors: APIError, PaginateError

Returns all used (exist in confirmed transactions) addresses of the user's wallet.
If {paginate} is not `undefined` then results are returned in a paginated manner in chronological order of use on the blockchain. If {paginate} is out of range, `PaginateError` is returned.

### ergo.get_unused_addresses(): Promise\<Address[]>

Errors: APIError

Returns unused addresses available that have not been used yet. The wallet would need to pre-generate addresses here within its restrictions (e.g. discover gap on indices for HD wallets).

### ergo.get_change_address(): Promise\<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 connnected wallet. This can be used as a generic receive address as well.

### ergo.sign_tx(tx: Tx) -> Promise\<SignedTx>

Errors: APIError, TxSignError

A signed tx is returned if the wallet can provide proofs for all inputs (P2PK ones). If not it produces an error. This should also have some kind of user pop-up which if rejected would produce an error as well. This way signing/keys are kept entirely in the wallet.

### ergo.sign_tx_input(tx: Tx, index: number): Promise\<SignedInput>

Errors: APIError, TxSignError

Lower level tx signing functionality that signs a single input if the wallet is able to, and returns an error if not. This can be useful for constructing transactions consisting of inputs from multiple parties, and should be general enough for future transaction signing use-cases. Likewise with `sign_tx` this should ask for user consent and can be rejected producing an error.

### ergo.sign_data(addr: Address, message: String): Promise\<String>

Errors: APIError, DataSignError

Signs the generic data {data} encoded as a hex string using the private key associated with {addr} if possible. If the wallet does not own this address, an error is returned. This should ask for user consent and produce an error if rejected. The wallet should also implement a message signing protocol such as the proposed [EmIP-005](https://github.com/Emurgo/EmIPs/blob/5b00fce84f31eb763892186eb9c88739ec809333/specs/emip-005.md) in order to make this endpoint safer for use, and make it harder for the user to accidentally sign data such as transactions, blocks, etc. Returns the signed data as a hex-encoded string.

TBD: Hash algorithm/format to use.

### ergo.submit_tx(tx: SignedTx): Promise\<TxId>

Errors: APIError, TxSendError

Uses the wallet to send the given tx and returns the TxId for the dApp to track when/if it is included as its own responsibility. This is technically not mandatory but any wallet that is using such a bridge should have this functionality. Wallets can additionally choose to rate-limit this call, and can return an error if they did not or could not send the transaction.


## Data Formats

The initial design for most of these types are meant to be the same as the JSON types in the Ergo full node, which matches up as of now with the `sigma-rust` representations, but these definitions are the definitive types.

### Address

String as the standard base58 encoding of an address.

### Box

Transaction output box. Format is the same as `sigma-rust`:
Copy link
Member

Choose a reason for hiding this comment

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

There is no Box in sigma-rust. I suppose ErgoBox is meant here.

```
type Box = {|
boxId: BoxId,
value: Value,
ergoTree: ErgoTree,
assets: TokenAmount[],
additionalRegisters: {| [string]: Constant |},
creationHeight: number,
transactionId: TxId,
index: number,
|};
```
Additional registers have string ids "R4", "R5" to "R9".

### BoxId

Hex string of the box id.

### Constant

Uses `sigma-rust` rep - Hex-encoded bytes for `SigmaSerialize` of a constant.

### ContextExtension

Uses the full node JSON rep (id to hex-encoded Sigma-state value). Empty object is for P2PK.
```
type ContextExtension = {||} | {|
values: {| [string]: string |}
|};
```
or the empty `{}` object for P2PK inputs.

### ErgoTree

Uses `sigma-rust` rep - Hex string representing serialized bytes of the tree using `SigmaSerialize`

### DataInput

Read-only input (e.g. oracle usage) that is not spendable. Uses `sigma-rust` JSON rep:
```
type DataInput = {|
boxId: BoxId,
|};
```

### SignedInput

A signed `UnsignedInput`. Uses full-node JSON rep:
```
type SignedInput = {|
boxId: BoxId,
spendingProof: ProverResult,
|};
```

### UnsignedInput

Uses full-node JSON rep:
```
type UnsignedInput = {|
boxId: BoxId,
extension: ContextExtension,
|};
rooooooooob marked this conversation as resolved.
Show resolved Hide resolved
```

## Paginate

```
type Paginate = {|
page: number,
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.

### ProverResult
Uses `sigma-rust` rep - proof is a byte array serialized using `SigmaSerialize` in hex format:
```
type ProverResult = {|
proofBytes: string,
extension: ContextExtension,
|};
```

### SignedTx

Uses `sigma-rust` rep for a transaction:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Uses `sigma-rust` rep for a transaction:
Uses `sigma-rust` `Transaction` rep for a transaction:


```
type SignedTx = {|
id: TxId,
inputs: SignedInput[],
dataInputs: DataInput[],
outputs: Box[],
size: number,
|};
```
with the difference with `Tx` is that the inputs are signed (`SignedInput` instead of `UnsignedInput`)


### TokenAmount

Uses `sigma-rust` rep:
```
type TokenAmount = {|
tokenId: TokenId,
amount: Value,
|};
```

### TokenId

Uses `sigma-rust` rep - Hex string of the id.

### Tx

An unsigned transaction. Uses a modified version of the transaction representation in `sigma-rust`.
```
type Tx = {|
id: TxId,
inputs: UnsignedInput[],
dataInputs: DataInput[],
outputs: Box[],
|};
```
This differs from `Tx` in that that `inputs` would be `[UnsignedInput]`, however we haven't signed this tx yet and also the outputs are merely candidates.

### TxId

Same as `sigma-rust` - string hex encoding

### Value

BigNum-like object. Represents a value which may or may not be ERG. Value is in the smallest possible unit (e.g. nanoErg for ERG). It can be either a `number` or a `string` using standard unsigned integer text representation.

### TxSendError

```
TxSendErrorCode {
Refused: 1,
Failure: 2,
}
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,
}
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


### DataSignError

```
DataSignErrorCode {
ProofGeneration: 1,
AddressNotPK: 2,
UserDeclined: 3,
InvalidFormat: 4,
}
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.

### 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.

## PaginateError

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