Home Reference Source

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)
}