uport-connect
Version:
Library for integrating uPort into your app frontend
172 lines (156 loc) • 5.72 kB
JavaScript
import async from 'async'
import { isMNID, decode } from 'mnid'
import HttpProvider from 'ethjs-provider-http'
import { askProvider } from 'uport-transports/lib/transport/ui'
import { isMobile, hasWeb3 } from './util'
/**
* A web3 style provider which can easily be wrapped with uPort functionality.
* Builds on a base provider. Used in Connect to wrap a provider with uPort specific
* functionality.
*/
class UportSubprovider {
/**
* Instantiates a new wrapped provider
*
* @param {Object} args required arguments
* @param {Function} args.requestAddress function to get the address of a uPort identity.
* @param {Function} args.sendTransaction function to handle passing transaction information to a uPort application
* @param {Object} args.provider a web3 sytle provider
* @return {UportSubprovider} this
*/
constructor({ requestAddress, sendTransaction, signTypedData, personalSign, provider, network }) {
if (!provider) {
// Extend ethjs HTTP provider if none is given
this.provider = new HttpProvider(network.rpcUrl)
} else {
this.provider = provider
console.warn('Uport functionality may not be entirely compatible with custom providers.')
}
// Detect injected provider
if (hasWeb3()) {
// Distinguish between providers in mobile and other cases
// Metamask/mist etc. will give the option to use uport
// Mobile injected providers (coinbase wallet, etc.) will be used automatically
if (isMobile()) {
this.useInjectedProvider = true
} else {
this.hasInjectedProvider = true
}
}
this.network = network
this.getAddress = (cb) => {
if (this.address) return cb(null, this.address)
requestAddress().then(
address => {
const errorMatch = new Error('Address/Account received does not match the network your provider is configured for')
this.setAccount(address) ? cb(null, this.address) : cb(errorMatch)
},
error => cb(error))
}
this.sendTransaction = (txobj, cb) => {
sendTransaction(txobj).then(
address => cb(null, address),
error => cb(error)
)
}
this.signTypedData = (typedData, cb) => {
signTypedData(typedData).then(
payload => cb(null, encodeSignature(payload.signature)),
error => cb(error)
)
}
this.personalSign = (data, cb) => {
personalSign(data).then(
payload => cb(null, encodeSignature(payload.signature)),
error => cb(error)
)
}
}
setAccount(address) {
if (this.network.id && isMNID(address)) {
const mnid = decode(address)
if (this.network.id === mnid.network) {
this.address = mnid.address
return true
}
return false
}
// Does not force validation, if no network id given will still set address
this.address = isMNID(address) ? decode(address).address : address
return true
}
/**
* Replace sync send with async send
* @private
*/
send(payload, callback) {
return this.sendAsync(payload, callback)
}
/**
* Overrides sendAsync to caputure the following RPC calls eth_coinbase, eth_accounts,
* and eth_sendTransaction. All other calls are passed to the based provider.
* eth_coinbase, eth_accounts will get a uPort identity address with getAddress.
* While eth_sendTransaction with send transactions to a uPort app with sendTransaction
*
* @param {Any} payload request payload
* @param {Function} callback called with response or error
* @private
*/
async sendAsync(payload, callback) {
let remember, useInjectedProvider = this.useInjectedProvider
// Present a dialog to ask about using injected provider if present but not approved
if (this.hasInjectedProvider && !this.useInjectedProvider) {
({ remember, useInjectedProvider } = await askProvider(payload.method === 'eth_sendTransaction'))
if (remember) this.useInjectedProvider = useInjectedProvider
}
// Use injected provider if present and approved
if (useInjectedProvider) {
web3.provider.sendAsync(payload, callback)
return
}
const respond = (error, result) => {
if (error) {
callback({
id: payload.id,
jsonrpc: '2.0',
error: error.message
})
} else {
callback(null, {
id: payload.id,
jsonrpc: '2.0',
result
})
}
}
if (Array.isArray(payload)) {
async.map(payload, this.sendAsync.bind(this), callback)
return
}
switch (payload.method) {
// TODO consider removing, not necessary for interaction with uport
case 'eth_coinbase':
return this.getAddress(respond)
case 'eth_accounts':
return this.getAddress((error, address) => {
respond(error, [address])
})
case 'eth_sendTransaction':
let txParams = payload.params[0]
return this.sendTransaction(txParams, respond)
case 'eth_signTypedData_v3':
case 'eth_signTypedData':
let typedData = payload.params[0]
return this.signTypedData(typedData, respond)
case 'personal_sign':
let data = payload.params[0]
return this.personalSign(data, respond)
default:
return this.provider.sendAsync(payload, callback)
}
}
}
export function encodeSignature({ r, s, v }) {
return `0x${r.padStart(64, '0')}${s.padStart(64, '0')}${v}`
}
export default UportSubprovider