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

Use ContractKit to get addresses for Blockchain API #1175

Merged
merged 20 commits into from
Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 1 addition & 3 deletions packages/blockchain-api/.env
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
EXCHANGE_RATES_API=https://api.exchangeratesapi.io
BLOCKSCOUT_API=https://alfajoresstaging-blockscout.celo-testnet.org/api
CELO_GOLD_ADDRESS=0x1313e2f3EBef8f0d869EECEb796D55A066eEA863
CELO_DOLLAR_ADDRESS=0x2df4dd6bd1b26a8503f763506bdb8e7cf165f69e
FAUCET_ADDRESS=0xF4314cb9046bECe6AA54bb9533155434d0c76909
VERIFICATION_REWARDS_ADDRESS=0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5
ATTESTATIONS_ADDRESS=0x8b7649116f169d2d2aebb6ea1a77f0baf31f2811
WEB3_PROVIDER_URL=https://integration-infura.celo-testnet.org/
5 changes: 1 addition & 4 deletions packages/blockchain-api/app.alfajores.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ env_variables:
DEPLOY_ENV: "alfajores"
EXCHANGE_RATES_API: "https://api.exchangeratesapi.io"
BLOCKSCOUT_API: "https://alfajores-blockscout.celo-testnet.org/api"
# Pull addresses from the build artifacts of the network in protocol/build
CELO_GOLD_ADDRESS: "0x11CD75C45638Ec9f41C0e8Df78fc756201E48ff2"
CELO_DOLLAR_ADDRESS: "0xd4b4fcaCAc9e23225680e89308E0a4C41Dd9C6B4"
FAUCET_ADDRESS: "0xCEa3eF8e187490A9d85A1849D98412E5D27D1Bb3"
VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5"
ATTESTATIONS_ADDRESS: "0x714f2879A4aa985508537f851FeBCfB26D7aF40D"
WEB3_PROVIDER_URL: "https://alfajores-infura.celo-testnet.org/"
5 changes: 1 addition & 4 deletions packages/blockchain-api/app.alfajoresstaging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ env_variables:
DEPLOY_ENV: "alfajoresstaging"
EXCHANGE_RATES_API: "https://api.exchangeratesapi.io"
BLOCKSCOUT_API: "https://alfajoresstaging-blockscout.celo-testnet.org/api"
# Pull addresses from the build artifacts of the network in protocol/build
CELO_GOLD_ADDRESS: "0x1313e2f3EBef8f0d869EECEb796D55A066eEA863"
CELO_DOLLAR_ADDRESS: "0x2dF4dD6Bd1b26a8503F763506bdB8e7cf165f69E"
FAUCET_ADDRESS: "0xF4314cb9046bECe6AA54bb9533155434d0c76909"
VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5"
ATTESTATIONS_ADDRESS: "0x8B7649116f169D2D2aeBB6Ea1A77F0baF31F2811"
WEB3_PROVIDER_URL: "https://alfajoresstaging-infura.celo-testnet.org/"
12 changes: 0 additions & 12 deletions packages/blockchain-api/app.dev.yaml

This file was deleted.

5 changes: 1 addition & 4 deletions packages/blockchain-api/app.integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ env_variables:
DEPLOY_ENV: "integration"
EXCHANGE_RATES_API: "https://api.exchangeratesapi.io"
BLOCKSCOUT_API: "https://integration-blockscout.celo-testnet.org/api"
# Pull addresses from the build artifacts of the network in protocol/build
CELO_GOLD_ADDRESS: "0x9102eCD93ac8D66bAc3D397BF52bc57Ee34Bcb87"
CELO_DOLLAR_ADDRESS: "0x47736AB66b892b0FCCb5c7d69B879C6141F6E80c"
FAUCET_ADDRESS: "0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95"
VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5"
ATTESTATIONS_ADDRESS: "0xD2C0894D551D4C810Dc5CB55081c0b7BE965A118"
WEB3_PROVIDER_URL: "https://integration-infura.celo-testnet.org/"
5 changes: 1 addition & 4 deletions packages/blockchain-api/app.pilot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ env_variables:
DEPLOY_ENV: "pilot"
EXCHANGE_RATES_API: "https://api.exchangeratesapi.io"
BLOCKSCOUT_API: "https://pilot-blockscout.celo-testnet.org/api"
# Pull addresses from the build artifacts of the network in protocol/build
CELO_GOLD_ADDRESS: "0xa69c3D18a74B3FD5F8aDA748428d0bfF8c5387Fe"
CELO_DOLLAR_ADDRESS: "0x996e24D7791A182f237635018c49E30cdA8FBa5e"
FAUCET_ADDRESS: "0x387bCb16Bfcd37AccEcF5c9eB2938E30d3aB8BF2"
VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5"
ATTESTATIONS_ADDRESS: "0x2cDEc3af5727dF2d490cF6068980E67dc6c19438"
WEB3_PROVIDER_URL: "https://pilot-infura.celo-testnet.org/"
13 changes: 0 additions & 13 deletions packages/blockchain-api/app.pilotstaging.yaml

This file was deleted.

1 change: 1 addition & 0 deletions packages/blockchain-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"deploy": "./deploy.sh"
},
"dependencies": {
"@celo/contractkit": "0.1.6",
Copy link
Contributor

Choose a reason for hiding this comment

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

@annakaz this isn't available on npm so we should use 0.1.5

Copy link
Contributor Author

Choose a reason for hiding this comment

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

0.1.6 has now been published, so leaving this version as is
https://www.npmjs.com/package/@celo/contractkit

"apollo-datasource-rest": "^0.3.1",
"apollo-server-express": "^2.4.2",
"bignumber.js": "^7.2.0",
Expand Down
59 changes: 43 additions & 16 deletions packages/blockchain-api/src/blockscout.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { RESTDataSource } from 'apollo-datasource-rest'
import BigNumber from 'bignumber.js'
import {
ATTESTATIONS_ADDRESS,
BLOCKSCOUT_API,
CONTRACT_SYMBOL_MAPPING,
FAUCET_ADDRESS,
VERIFICATION_REWARDS_ADDRESS,
} from './config'
import { BLOCKSCOUT_API, FAUCET_ADDRESS, VERIFICATION_REWARDS_ADDRESS } from './config'
import { EventArgs, EventInterface, EventTypes, TransferEvent } from './schema'
import { formatCommentString } from './utils'
import { formatCommentString, getContractAddresses } from './utils'

// to get rid of 18 extra 0s in the values
const WEI_PER_GOLD = Math.pow(10, 18)
Expand Down Expand Up @@ -51,6 +45,8 @@ export interface BlockscoutTransaction {
}

export class BlockscoutAPI extends RESTDataSource {
tokenAddressMapping: { [key: string]: string } | undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason why we cache these values here in this class and also in the utils file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I cached these both places to avoid passing them back and forth. I realize this isn't the cleanest solution, but the alternatives of:

  • Only caching them in the class, and passing them (sometimes undefined) in as parameters to the getter function is a weird pattern
  • Only caching them in the function, and needing to call and store the addresses in the class each time they're used
    Seemed less desirable

Copy link
Contributor

@jmrossy jmrossy Oct 4, 2019

Choose a reason for hiding this comment

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

Only caching them in the function, and needing to call and store the addresses

Maybe I'm missing something but why does the class need to store the addresses at all when they'll always be available in util's module-level cache after first retrieval? Can't we just call get every time we want them?

attestationsAddress: string | undefined
constructor() {
super()
this.baseURL = BLOCKSCOUT_API
Expand All @@ -67,6 +63,33 @@ export class BlockscoutAPI extends RESTDataSource {
return result
}

async ensureTokenAddresses() {
if (this.tokenAddressMapping && this.attestationsAddress) {
// Already got addresses
return
} else {
const addresses = await getContractAddresses()
this.attestationsAddress = addresses.attestationsAddress
this.tokenAddressMapping = addresses.tokenAddressMapping
}
}

getTokenAtAddress(tokenAddress: string) {
if (this.tokenAddressMapping) {
return this.tokenAddressMapping[tokenAddress]
} else {
throw new Error('Cannot find tokenAddressMapping despite intialization')
}
}

getAttestationAddress() {
if (this.attestationsAddress) {
return this.attestationsAddress
} else {
throw new Error('Cannot find attestation address despite intialization')
}
}

// LIMITATION:
// This function will only return Gold transfers that happened via the GoldToken
// contract. Any native transfers of Gold will be omitted because of how blockscout
Expand All @@ -89,6 +112,7 @@ export class BlockscoutAPI extends RESTDataSource {
txHashToEventTransactions.set(tx.hash, currentTX)
}

await this.ensureTokenAddresses()
// Generate final events
txHashToEventTransactions.forEach((transactions: BlockscoutTransaction[], txhash: string) => {
// Exchange events have two corresponding transactions (in and out)
Expand All @@ -106,9 +130,9 @@ export class BlockscoutAPI extends RESTDataSource {
type: EventTypes.EXCHANGE,
timestamp: new BigNumber(inEvent.timeStamp).toNumber(),
block: new BigNumber(inEvent.blockNumber).toNumber(),
inSymbol: CONTRACT_SYMBOL_MAPPING[inEvent.contractAddress.toLowerCase()],
inSymbol: this.getTokenAtAddress(inEvent.contractAddress.toLowerCase()),
inValue: new BigNumber(inEvent.value).dividedBy(WEI_PER_GOLD).toNumber(),
outSymbol: CONTRACT_SYMBOL_MAPPING[outEvent.contractAddress.toLowerCase()],
outSymbol: this.getTokenAtAddress(outEvent.contractAddress.toLowerCase()),
outValue: new BigNumber(outEvent.value).dividedBy(WEI_PER_GOLD).toNumber(),
hash: txhash,
})
Expand All @@ -122,7 +146,8 @@ export class BlockscoutAPI extends RESTDataSource {
const [type, address] = resolveTransferEventType(
userAddress,
eventToAddress,
eventFromAddress
eventFromAddress,
this.getAttestationAddress()
)
events.push({
type,
Expand All @@ -131,7 +156,7 @@ export class BlockscoutAPI extends RESTDataSource {
value: new BigNumber(event.value).dividedBy(WEI_PER_GOLD).toNumber(),
address,
comment,
symbol: CONTRACT_SYMBOL_MAPPING[event.contractAddress.toLowerCase()] || 'unknown',
symbol: this.getTokenAtAddress(event.contractAddress.toLowerCase()) || 'unknown',
hash: txhash,
})
}
Expand All @@ -148,6 +173,7 @@ export class BlockscoutAPI extends RESTDataSource {
async getFeedRewards(args: EventArgs) {
const rewards: TransferEvent[] = []
const rawTransactions = await this.getTokenTransactions(args)
await this.ensureTokenAddresses()
for (const t of rawTransactions) {
// Only include verification rewards transfers
if (t.from.toLowerCase() !== VERIFICATION_REWARDS_ADDRESS) {
Expand All @@ -160,7 +186,7 @@ export class BlockscoutAPI extends RESTDataSource {
value: new BigNumber(t.value).dividedBy(WEI_PER_GOLD).toNumber(),
address: VERIFICATION_REWARDS_ADDRESS,
comment: t.input ? formatCommentString(t.input) : '',
symbol: CONTRACT_SYMBOL_MAPPING[t.contractAddress],
symbol: this.getTokenAtAddress(t.contractAddress),
hash: t.hash,
})
}
Expand All @@ -176,13 +202,14 @@ export class BlockscoutAPI extends RESTDataSource {
function resolveTransferEventType(
userAddress: string,
eventToAddress: string,
eventFromAddress: string
eventFromAddress: string,
attestationsAddress: string
): [EventTypes, string] {
if (eventToAddress === userAddress && eventFromAddress === FAUCET_ADDRESS) {
return [EventTypes.FAUCET, FAUCET_ADDRESS]
}
if (eventToAddress === ATTESTATIONS_ADDRESS && eventFromAddress === userAddress) {
return [EventTypes.VERIFICATION_FEE, ATTESTATIONS_ADDRESS]
if (eventToAddress === attestationsAddress && eventFromAddress === userAddress) {
return [EventTypes.VERIFICATION_FEE, attestationsAddress]
}
if (eventToAddress === userAddress && eventFromAddress === VERIFICATION_REWARDS_ADDRESS) {
return [EventTypes.VERIFICATION_REWARD, VERIFICATION_REWARDS_ADDRESS]
Expand Down
9 changes: 1 addition & 8 deletions packages/blockchain-api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,9 @@ import dotenv from 'dotenv'
// Load environment variables from .env file
dotenv.config()

export const CONTRACT_SYMBOL_MAPPING: { [key: string]: string } = {
// GOLD_CONTRACT_ADDRESS
[(process.env.CELO_GOLD_ADDRESS as string).toLowerCase()]: 'Celo Gold',
// DOLLAR_CONTRACT_ADDRESS
[(process.env.CELO_DOLLAR_ADDRESS as string).toLowerCase()]: 'Celo Dollar',
}

export const EXCHANGE_RATES_API = (process.env.EXCHANGE_RATES_API as string).toLowerCase()
export const BLOCKSCOUT_API = (process.env.BLOCKSCOUT_API as string).toLowerCase()
export const FAUCET_ADDRESS = (process.env.FAUCET_ADDRESS as string).toLowerCase()
export const VERIFICATION_REWARDS_ADDRESS = (process.env
.VERIFICATION_REWARDS_ADDRESS as string).toLowerCase()
export const ATTESTATIONS_ADDRESS = (process.env.ATTESTATIONS_ADDRESS as string).toLowerCase()
export const WEB3_PROVIDER_URL = process.env.WEB3_PROVIDER_URL
52 changes: 52 additions & 0 deletions packages/blockchain-api/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* tslint:disable:no-console */
import { CeloContract, ContractKit, newKitFromWeb3 } from '@celo/contractkit'
import * as utf8 from 'utf8'
import Web3 from 'web3'
import coder from 'web3-eth-abi'
import { WEB3_PROVIDER_URL } from './config'

export function randomTimestamp() {
const start = new Date(2018, 0, 1)
Expand Down Expand Up @@ -40,3 +43,52 @@ export function formatCommentString(functionCallHex: string): string {
export function formatDateString(date: Date) {
return date.toISOString().split('T')[0]
}

let goldTokenAddress: string
let stableTokenAddress: string
let attestationsAddress: string
let tokenAddressMapping: { [key: string]: string }
export async function getContractAddresses() {
if (goldTokenAddress && stableTokenAddress && attestationsAddress) {
Copy link
Contributor

Choose a reason for hiding this comment

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

some error handling around this function contents would be good.

console.info('Already got token addresses')
return { tokenAddressMapping, attestationsAddress }
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: No need for this else, the if statement returns
See here:
https://stackoverflow.com/questions/268132/invert-if-statement-to-reduce-nesting

try {
const kit = await getContractKit()
goldTokenAddress = await kit.registry.addressFor(CeloContract.StableToken)
stableTokenAddress = await kit.registry.addressFor(CeloContract.GoldToken)
attestationsAddress = await kit.registry.addressFor(CeloContract.Attestations)
tokenAddressMapping = {
[goldTokenAddress]: 'Celo Gold',
[stableTokenAddress]: 'Celo Dollar',
}
console.info('Got token addresses' + attestationsAddress)
return { tokenAddressMapping, attestationsAddress }
} catch (e) {
console.error('@getContractAddresses() error', e)
throw new Error('Unable to fetch contract addresses')
}
}
}

let contractKit: ContractKit
export async function getContractKit(): Promise<ContractKit> {
if (contractKit && (await contractKit.isListening())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto here

// Already connected
return contractKit
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

try {
if (WEB3_PROVIDER_URL) {
const httpProvider = new Web3.providers.HttpProvider(WEB3_PROVIDER_URL)
const web3 = new Web3(httpProvider)
contractKit = newKitFromWeb3(web3)
return contractKit
} else {
throw new Error('Missing web3 provider URL, will not be able to fetch contract addresses.')
}
} catch (e) {
console.error('@getContractKit() error', e)
throw new Error('Failed to create contractKit instance')
}
}
}
16 changes: 16 additions & 0 deletions packages/blockchain-api/test/blockscout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ jest.mock('../src/config.ts', () => {
}
})

jest.mock('../src/utils.ts', () => {
const contractGetter = jest.fn()
const tokenAddressMapping: { [key: string]: string } = {
['0x000000000000000000000000000000000000gold']: 'Celo Gold',
['0x0000000000000000000000000000000000dollar']: 'Celo Dollar',
}
contractGetter.mockReturnValue({
tokenAddressMapping,
attestationsAddress: '0x0000000000000000000000000000000000a77357',
})
return {
...jest.requireActual('../src/utils.ts'),
getContractAddresses: contractGetter,
}
})

describe('Blockscout', () => {
let blockscoutAPI: BlockscoutAPI

Expand Down
Loading