src/light.ts
const NH = require('eth-ens-namehash')
import axios from 'axios'
const bs58 = require('bs58')
const sha256 = require('sha256')
import * as SvConsts from './const'
import * as SvUtils from './utils'
import * as StellarBase from 'stellar-base'
import * as assert from 'assert'
import * as web3Utils from 'web3-utils'
// Lovely ABIs
import ResolverAbi from './smart_contracts/SV_ENS_Resolver.abi.json'
import IndexAbi from './smart_contracts/SVLightIndex.abi.json'
import BackendAbi from './smart_contracts/SVLightIndexBackend.abi.json'
import BBFarmAbi from './smart_contracts/BBFarm.abi.json'
import PaymentsAbi from './smart_contracts/SVPayments.abi.json'
import AuxAbi from './smart_contracts/AuxAbi.abi.json'
import AuctionAbi from './smart_contracts/CommAuctionIface.abi.json'
import ERC20Abi from './smart_contracts/ERC20.abi.json'
import UnsafeEd25519DelegationAbi from './smart_contracts/UnsafeEd25519Delegation.abi.json'
export const initializeSvLight = async svConfig => {
const { indexContractName, ensResolver, httpProvider, auxContract } = svConfig
const Web3 = require('web3')
const web3 = new Web3(new Web3.providers.HttpProvider(httpProvider))
const resolver = new web3.eth.Contract(ResolverAbi, ensResolver)
// const indexAddress =
// console.log('indexAddress :', indexAddress);
const index = new web3.eth.Contract(IndexAbi, await resolveEnsAddress({ resolver }, indexContractName))
const backendAddress = await getBackendAddress({ index })
const backend = new web3.eth.Contract(BackendAbi, backendAddress)
const aux = new web3.eth.Contract(AuxAbi, auxContract)
const payments = new web3.eth.Contract(PaymentsAbi, await index.methods.getPayments().call())
return {
svConfig,
web3,
resolver,
index,
backend,
aux,
payments
}
}
export const resolveEnsAddress = async ({ resolver }, ensName) => {
return await resolver.methods.addr(NH.hash(ensName)).call()
}
export const getBackendAddress = async ({ index }) => {
return await index.methods.getBackend().call()
}
export const getDemocInfo = async ({ backend, democHash }) => {
return await backend.methods.getDInfo(democHash).call()
}
export const getDemocNthBallot = async ({ svNetwork }, democBallotInfo) => {
// Destructure and set the variables that are needed
const { index, backend, aux, svConfig } = svNetwork
const { democHash, nthBallot } = democBallotInfo
const indexAddress = index._address
const backendAddress = backend._address
const archiveUrl = { svConfig }
const bbFarmAndBallotId = await aux.methods.getBBFarmAddressAndBallotId(backendAddress, indexAddress, democHash, nthBallot).call()
// console.log('bbFarmAndBallotId :', bbFarmAndBallotId);
const { id, bbFarmAddress } = bbFarmAndBallotId
const userEthAddress = '0x0000000000000000000000000000000000000000'
const ethBallotDetails = await aux.methods.getBallotDetails(id, bbFarmAddress, userEthAddress).call()
const ballotSpec = await getBallotSpec(archiveUrl, ethBallotDetails.specHash)
// console.log('ballotSpec :', ballotSpec);
// .then(x => console.log('Then called', x))
// .catch(x => console.log('Caught error', x));
const ballotObject = {
...bbFarmAndBallotId,
...ethBallotDetails,
data: { ...ballotSpec.data }
}
return ballotObject
}
export const getBallotSpec = async (archiveUrl, ballotSpecHash): Promise<{ data: any }> => {
// TODO refactor to be a bit more elegant
return new Promise<{ data: any }>((res, rej) => {
let done = false
const doRes = obj => {
if (!done) {
done = true
res(obj)
}
}
getBallotObjectFromIpfs(ballotSpecHash).then(doRes)
setTimeout(() => {
if (!done) {
getBallotObjectFromS3(archiveUrl, ballotSpecHash)
.then(doRes)
.catch(rej)
}
}, 3500)
})
}
export const getBallotObjectFromS3 = async (archiveUrl, ballotSpecHash) => {
return axios.get(archiveUrl + ballotSpecHash + '.json')
}
export const getBallotObjectFromIpfs = async ballotSpecHash => {
const ipfsUrl = 'https://ipfs.infura.io/api/v0/block/get?arg='
const cidHex = '1220' + ballotSpecHash.substr(2)
const bytes = Buffer.from(cidHex, 'hex')
const cid = bs58.encode(bytes)
return await axios.get(ipfsUrl + cid)
}
// Take the svNetwork object and a democHash, will return all of the ballots from the democracy in an array
export const getDemocBallots = async ({ svNetwork, democHash }) => {
const { backend } = svNetwork
const democInfo = await getDemocInfo({ backend, democHash })
// Throw an error if the democ info is not correct
const { erc20, owner } = democInfo
if (owner === '0x0000000000000000000000000000000000000000') {
throw new Error('Democracy Hash does not resolve to a democracy')
}
// TODO - Work out where / how to push an errored ballot
// Loop through and get all the ballots
const numBallots = democInfo.nBallots
const ballotsArray = []
for (let i = 0; i < numBallots; i++) {
ballotsArray[i] = await getDemocNthBallot({ svNetwork }, { democHash: democHash, nthBallot: i })
}
return ballotsArray
}
/** Takes in the svNetwork object and returns all relevant addresses */
export const getContractAddresses = async ({ svNetwork }) => {
const { index, resolver, backend, aux, svConfig } = svNetwork
const { delegationContractName, lookupAddress } = svConfig
return {
indexAddress: index._address,
backendAddress: backend._address,
auxAddress: aux._address,
lookupAddress: lookupAddress,
resolverAddress: resolver._address,
communityAuctionAddress: await index.methods.getCommAuction().call(),
delegationAddress: await resolveEnsAddress({ resolver }, delegationContractName),
paymentsAddress: await index.methods.getPayments().call()
}
}
export const weiToCents = async ({ payments }, wei) => {
return await payments.methods.weiToCents(wei).call()
}
export const getCommunityBallotPrice = async ({ payments }, democHash) => {
return await payments.methods.getNextPrice(democHash).call()
}
export const checkIfAddressIsEditor = async ({ svNetwork }, { userAddress, democHash }) => {
const { backend } = svNetwork
return await backend.methods.isDEditor(democHash, userAddress).call()
}
// Checks the current ethereum gas price and returns a couple of values
export const getCurrentGasPrice = async () => {
const gasStationInfo = await axios.get('https://ethgasstation.info/json/ethgasAPI.json')
const { data } = gasStationInfo
return {
safeLow: data.safeLow / 10,
average: data.average / 10,
fast: data.fast / 10,
fastest: data.fastest / 10
}
}
/**
* Verify a BallotSpec's hash
*
* @param {*} rawBallotSpecString The raw string/bytes before JSON.parse
* @param {*} expectedSpecHash The expected hash as Eth Hex
*
* @returns {boolean} Whether the ballotSpec matched the expected hash
*/
export const checkBallotHashBSpec = (rawBallotSpecString, expectedSpecHash) => {
throw Error('Unimplemented (check code for details)')
// NOTE: This function is unsafe - JSON does not have deterministic key order
// a ballotSpec object is not suitable to verify the hash; you need the _raw_
// string before it is parsed to JSON
// Original function
// let contentHash = '0x' + sha256(JSON.stringify(ballotSpec, null, 2))
// if (assertSpecHash === contentHash) {
// return true
// } else {
// return false
// }
}
// Checks the ballot hash against a ballot global ballot object
// Does this by destructuring the specHash and data out of it
export const checkBallotHashGBallot = ballotObject => {
const { data, specHash } = ballotObject
return checkBallotHashBSpec(data, specHash)
}
// Takes the name of an abi and a method name
// Returns a new ABI array with only the requested method
export const getSingularCleanAbi = (requestedAbiName, methodName) => {
const abiList = {
ResolverAbi: ResolverAbi,
IndexAbi: IndexAbi,
BackendAbi: BackendAbi,
BBFarmAbi: BBFarmAbi,
PaymentsAbi: PaymentsAbi,
AuxAbi: AuxAbi,
AuctionAbi: AuctionAbi,
ERC20Abi: ERC20Abi
}
const selectedAbi = abiList[requestedAbiName]
const methodObject = selectedAbi.filter(abi => abi.name == methodName)
return methodObject
}
export const stellarPkToHex = (pubKey: string): string => {
// Get the hex pub key
let rawPubKey, hexPubKey
if (web3Utils.isHex(pubKey)) {
hexPubKey = web3Utils.isHexStrict(pubKey) ? pubKey : '0x' + pubKey
} else {
const kp = StellarBase.Keypair.fromPublicKey(pubKey)
const rawPubKey = kp.rawPublicKey()
const hexPubKey = '0x' + rawPubKey.toString('hex')
}
return hexPubKey
}
/**
*
* @param pubKey
* @param svNetwork
*/
export const getUnsafeEd25519Delegations = async (pubKey: string, svNetwork) => {
// TODO - Some assertions and stuff..
const { web3, svConfig } = svNetwork
const { unsafeEd25519DelegationAddr } = svConfig
const Ed25519Del = new web3.eth.Contract(UnsafeEd25519DelegationAbi, unsafeEd25519DelegationAddr)
const delegations = await Ed25519Del.methods
.getAllForPubKey(stellarPkToHex(pubKey))
.call()
.catch(error => {
throw error
})
console.log('Fresh:', delegations)
return delegations
}
/**
* Generate a packed Ed25519Delegation instruction for use with the smart contract or API
* @param address An ethereum address to delegate to
* @param nonce A nonce in hex that is 3 bytes (6 characters as hex)
* @returns {Bytes32} The hex string (with 0x prefix) of the delegation instruction
*/
export const prepareEd25519Delegation = (address: string, nonce?: string) => {
// Delegate prefix (SV-ED-ETH)
const prefix = SvUtils.cleanEthHex(web3Utils.toHex(SvConsts.Ed25519DelegatePrefix))
const _nonce = nonce && web3Utils.isHex(nonce) ? nonce : web3Utils.randomHex(3).slice(2)
const trimmedAddress = SvUtils.cleanEthHex(address)
const dlgtPacked = `0x${prefix}${_nonce}${trimmedAddress}`.toLowerCase()
assert.equal(dlgtPacked.length, 2 + 64, 'dlgtPacked was not 32 bytes / 64 chars long. This should never happen.')
return dlgtPacked
}
/**
* Create a tx object for an ed25519 delegation
* @param svNetwork
* @param dlgtRequest
* @param pubKey
* @param signature
* @param privKey
* @returns {to: string, value: number, gas: number, data: string}
*/
export const createEd25519DelegationTransaction = (
svNetwork: any,
dlgtRequest: string,
pubKey: string,
signature: string,
privKey: string
) => {
const { web3, svConfig } = svNetwork
const { unsafeEd25519DelegationAddr } = svConfig
// Initialise the contract
const Ed25519Del = new web3.eth.Contract(UnsafeEd25519DelegationAbi, unsafeEd25519DelegationAddr)
// Split the 64 bytes of the signature into an array containging 2x bytes32
const sig1 = `0x${signature.slice(0, 64)}`
const sig2 = `0x${signature.slice(64)}`
const addDelegation = Ed25519Del.methods.addUntrustedSelfDelegation(dlgtRequest, stellarPkToHex(pubKey), [sig1, sig2])
const txData = addDelegation.encodeABI()
return {
to: unsafeEd25519DelegationAddr,
value: 0,
gas: 500000,
data: txData
}
// .then(x => {
// const { rawTransaction } = x
// web3.eth
// .sendSignedTransaction(rawTransaction)
// .on('receipt', receipt => {
// const { transactionHash } = receipt
// resolve(transactionHash)
// })
// .catch(error => reject(error))
// })
// .catch(error => reject(error))
}
/**
* Verify an ed25519 self-delegation
* @param dlgtRequest eth hex string of the dlgt request
* @param pubKey stellar pubkey
* @param signature 64 byte signature as eth hex
* @returns {boolean}
*/
export const ed25519DelegationIsValid = (dlgtRequest: string, pubKey: string, signature: string) => {
const _sig = SvUtils.cleanEthHex(signature)
assert.equal(_sig.length, 128, 'Invalid signature, should be a 64 byte hex string')
// Create the keypair from the public key
const kp = StellarBase.Keypair.fromPublicKey(pubKey)
// Create a buffer from the signature
const sigBuffer = Buffer.from(SvUtils.hexToUint8Array(_sig))
// Verify the request against the signature
return kp.verify(dlgtRequest, sigBuffer)
}