steemit / steem-uri-spec

steem:// signing spec and reference implementation
MIT License
14 stars 13 forks source link

Steem URI protocol

Protocol facilitating signing of steem transactions. Meant to be implemented by secure Steem wallet applications.

This repository contains both the specification and a zero dependency reference implementation that works in node.js and most browsers.

Installation

Via npm or yarn:

npm install steem-uri
yarn add steem-uri

Manually: clone the repository and run make, this will place the built lib in lib/index.js.

Example usage

Encoding operations:

const steemuri = require('steem-uri')

steemuri.encodeOp(['vote', {voter: 'foo', author: 'bar', permlink: 'baz', weight: 10000}])
// steem://sign/op/WyJ2b3RlIix7InZvdGVyIjoiZm9vIiwiYXV0aG9yIjoiYmFyIiwicGVybWxpbmsiOiJiYXoiLCJ3ZWlnaHQiOjEwMDAwfV0.

steemuri.encodeOps([
    ['vote', {voter: 'foo', author: 'bar', permlink: 'baz', weight: 10000}],
    ['transfer', {from: 'foo', to: 'bar', amount: '10.000 STEEM', memo: 'baz'}]
], {callback: 'https://example.com/wallet?tx={{id}}'})
// steem://sign/ops/W1sidm90ZSIseyJ2b3RlciI6ImZvbyIsImF1dGhvciI6ImJhciIsInBlcm1saW5rIjoiYmF6Iiwid2VpZ2h0IjoxMDAwMH1dLFsidHJhbnNmZXIiLHsiZnJvbSI6ImZvbyIsInRvIjoiYmFyIiwiYW1vdW50IjoiMTAuMDAwIFNURUVNIiwibWVtbyI6ImJheiJ9XV0.?cb=aHR0cHM6Ly9leGFtcGxlLmNvbS93YWxsZXQ_dHg9e3tpZH19

Decoding and resolving steem:// links (for wallet implementers):

const steemuri = require('steem-uri')

// parse the steem:// link
const parsed = steemuri.decode(link)

// resolve the decoded tx and params to a signable tx
let {tx, signer} = steemuri.resolveTransaction(parsed.tx, parsed.params, {
    // e.g. from a get_dynamic_global_properties call
    ref_block_num: 1234,
    ref_block_prefix: 5678900,
    expiration: '2020-01-01T00:00:00',
    // accounts we are able to sign for
    signers: ['foo', 'bar'],
    // selected signer if none is asked for by the params
    preferred_signer: 'foo',
})

// sign broadcast the transaction to the network
let signature = signTx(tx, myKeys[signer])
let confirmation
if (!parsed.params.no_broadcast) {
    tx.signatures = [signature]
    confirmation = broadcastTx(tx)
}

// redirect to the callback if set
if (parsed.params.callback) {
    let url = steemuri.resolveCallback(parsed.params.callback, {
        sig: signature,
        id: confirmation.id,
        block: confirmation.block_num,
        txn: confirmation.txn_num,
    })
    redirectTo(url)
}

Specification

A protocol that allows Steem transactions and operations to be encoded into links that can be shared across applications and devices to sign transactions without implementers having to reveal their private key.

Actions

To facilitate re-usable signing URIs the implementation allows for a set of placeholder variables that can be used in a signing payload.

*Reasonable values are up to the implementer, suggested expiry time is 60 seconds ahead of rpc node time and the reference block set to the current head block.

Parameters

Params are global to all actions and encoded as query string params.

Params uses short names to save space in encoded URIs.

Callbacks

Callbacks should be redirected to once the transaction has been accepted by the network. If the callback url is a web link only https should be allowed.

The callbacks also allow simple templating with some response parameters, the templating format is {{<param_name>}}, e.g. https://myapp.com/wallet?tx={{id}}&included_in={{block}} or mymobileapp://signed/{{sig}}

Callback template params:

*Will not be available if the nb param was set for the action.

Base64u

An URL-safe version base64 where + is replaced by -, / by _ and the = padding by ..

base64
SGn+dGhlcmUh/k5pY2X+b2b+eW91/nRv/mRlY29kZf5tZf46KQ==
base64u
SGn-dGhlcmUh_k5pY2X-b2b-eW91_nRv_mRlY29kZf5tZf46KQ..

JavaScript implementation:

b64u_lookup = {'/': '_', '_': '/', '+': '-', '-': '+', '=': '.', '.': '='}
b64u_enc = (str) => btoa(str).replace(/(\+|\/|=)/g, (m) => b64u_lookup[m])
b64u_dec = (str) => atob(str.replace(/(-|_|\.)/g, (m) => b64u_lookup[m]))

Specialized actions

To keep the length of the URIs short, and the QR code size manageable, some common operations have aliases. See list below for supported aliases, params noted in operation payloads using the format {{param_name}} and optional ([param_name]) params should be filled with empty strings unless otherwise specified:

Transfer tokens

Action: steem://sign/transfer/<username>/<amount>[/memo]

Params:

Operation:

["transfer", {
  "from": "__signer",
  "to": "{{username}}",
  "amount": "{{amount}}",
  "memo": "{{memo}}"
}]

Follow user

Action: steem://sign/follow/<username>

Params:

Operation:

["custom_json", {
  "required_auths": [],
  "required_posting_auths": ["__signer"],
  "id": "follow",
  "json": "[\"follow\",{\"follower\":\"__signer\",\"following\":\"{{username}}\",\"what\":[\"blog\"]}]"
}]

Examples

Example usage of the protocol along with data for every step that can be used in tests. Examples assumes a __signer of foo a __expiration of 1970-01-01T00:00:00 and __ref_block_num, __ref_block_prefix set to 0 unless otherwise stated.

Send a limit order

Might be requested from a trading app, here we don't use any templating since we never want the transaction to be reusable.

Transaction:

{
  "ref_block_num": 48872,
  "ref_block_prefix": 1543858519,
  "expiration": "2018-05-29T13:17:39",
  "extensions": [],
  "operations": [
    ["limit_order_create2", {
      "owner": "foo",
      "orderid": 1,
      "amount_to_sell": "10.000 STEEM",
      "fill_or_kill": false,
      "exchange_rate": {"base": "1.000 STEEM", "quote": "0.420 SBD"},
      "expiration": "2018-05-30T00:00:00"
    }]
  ]
}

Parameters:

{
  "signer": "foo",
  "callback": "https://steem.trader/sign_callback?id={{id}}"
}

Encoded:

steem://sign/tx/eyJyZWZfYmxvY2tfbnVtIjo0ODg3MiwicmVmX2Jsb2NrX3ByZWZpeCI6MTU0Mzg1ODUxOSwiZXhwaXJhdGlvbiI6IjIwMTgtMDUtMjlUMTM6MTc6MzkiLCJleHRlbnNpb25zIjpbXSwib3BlcmF0aW9ucyI6W1sibGltaXRfb3JkZXJfY3JlYXRlMiIseyJvd25lciI6ImZvbyIsIm9yZGVyaWQiOjEsImFtb3VudF90b19zZWxsIjoiMTAuMDAwIFNURUVNIiwiZmlsbF9vcl9raWxsIjpmYWxzZSwiZXhjaGFuZ2VfcmF0ZSI6eyJiYXNlIjoiMS4wMDAgU1RFRU0iLCJxdW90ZSI6IjAuNDIwIFNCRCJ9LCJleHBpcmF0aW9uIjoiMjAxOC0wNS0zMFQwMDowMDowMCJ9XV19?s=foo&cb=aHR0cHM6Ly9zdGVlbS50cmFkZXIvc2lnbl9jYWxsYmFjaz9pZD17e2lkfX0.

Witness vote

Reusable witness vote URI, e.g. for a "Vote for me!" QR code t-shirt.

Operation:

["account_witness_vote", {
  "account": "__signer",
  "witness": "jesta",
  "approve": true
}]

Encoded:

steem://sign/op/WyJhY2NvdW50X3dpdG5lc3Nfdm90ZSIseyJhY2NvdW50IjoiX19zaWduZXIiLCJ3aXRuZXNzIjoiamVzdGEiLCJhcHByb3ZlIjp0cnVlfV0.

Multisig

To sign for an account setup with multiple authorities a central service can act as a transaction facilitator using the nb (no_broadcast) option.

In the following scenario the account foo is setup with an active authority that has three account auths belonging to bob, alice and picard, the weights are setup so that two of those three accounts needs to sign.

bob wants to transfer 150.000 STEEM from the foo account to himself so he submits an operation to the signing service:

["transfer", {
  "from": "foo",
  "to": "bob",
  "amount": "150.000 STEEM",
  "memo": "Bob's boat needs plastic padding"
}]

The service then generates a signing URI with that operation and the following options:

{
  "no_broadcast": true,
  "callback": "https://sign.steem.vc/collect?id=123&sig={{sig}}"
}
steem://sign/op/WyJ0cmFuc2ZlciIseyJmcm9tIjoiZm9vIiwidG8iOiJib2IiLCJhbW91bnQiOiIxNTAuMDAwIFNURUVNIiwibWVtbyI6IkJvYidzIGJvYXQgbmVlZHMgcGxhc3RpYyBwYWRkaW5nIn1d?nb=&cb=aHR0cHM6Ly9zaWduLnN0ZWVtLnZjL2NvbGxlY3Q_aWQ9MTIzJnNpZz17e3NpZ319

bob then signs the transaction using the URI, the service callback is pinged and the service now has his signature. Then he sends the URI to alice and picard and when one of them signs it the service has enough signatures it broadcasts the transaction.

The UX of a service like this can be excellent with the help of QR codes and collecting emails for signers so they can be notified when a signature is needed and when the transaction is broadcast.