src/vc-status-registry.ts
/*
* Copyright 2020 Coöperatieve Rabobank U.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ethers, providers, Contract, Wallet } from 'ethers'
import { VcStatusRegistryOptions, ContractEventData, NewBlockData, PastEventType } from './interfaces'
import { TransactionCount } from '.'
import { Observable, Subject } from 'rxjs'
const ABI = require('./ABI.json')
export class VcStatusRegistry {
private readonly _provider: providers.JsonRpcProvider
private readonly _wallet: Wallet | undefined
private readonly _contract: Contract
private _transactionCount: TransactionCount | undefined
private _onNewBlock = new Subject<NewBlockData>()
private _onSetVcStatus = new Subject<ContractEventData>()
private _onRemoveVcStatus = new Subject<ContractEventData>()
private _onError = new Subject<any>()
/**
* @constructor Will set up the connection to an ethereum provider with provided credentials.
* @param _ethereumProvider connection string
* @param _contractAddress address of the contract '0x...'
* @param privateKey optional, private key for issuing credentials
* @param _options optional, see VcStatusRegistryOptions
*/
constructor (
private readonly _ethereumProvider: string,
private readonly _contractAddress: string,
privateKey?: string,
private readonly _options: VcStatusRegistryOptions = {}
) {
this._provider = new ethers.providers.JsonRpcProvider(this._ethereumProvider)
this._contract = new ethers.Contract(this._contractAddress, ABI, this._provider)
if (privateKey) {
this._wallet = new ethers.Wallet(Buffer.from(privateKey, 'hex'), this._provider)
this._transactionCount = new TransactionCount(this._wallet, this._options)
this._contract = this._contract.connect(this._wallet)
}
this.initiateStatusEventSubscriber(PastEventType.set)
this.initiateStatusEventSubscriber(PastEventType.remove)
this.initiateErrorEventSubscriber()
this.initiateNewBlockEventSubscriber()
}
get ethereumProvider (): string {
return this._ethereumProvider
}
get contractAddress (): string {
return this._contractAddress
}
get provider (): providers.JsonRpcProvider {
return this._provider
}
get wallet (): Wallet | undefined {
return this._wallet
}
get ABI (): object {
return ABI
}
get onNewBlock (): Observable<NewBlockData> {
return this._onNewBlock
}
get onSetVcStatus (): Observable<ContractEventData> {
return this._onSetVcStatus
}
get onRemoveVcStatus (): Observable<ContractEventData> {
return this._onRemoveVcStatus
}
get onError (): Observable<any> {
return this._onError
}
public setVcStatus = async (credentialId: string, value: boolean = true): Promise<string> => {
const method = value ? 'setVcStatus' : 'removeVcStatus'
if (!this._wallet) {
throw (new Error(`Error: Can not call "${method}" without privateKey`))
}
const nonce = await (this._transactionCount as TransactionCount).transactionCount()
const overrides = {
nonce: nonce,
gasPrice: this._options.gasPrice,
gasLimit: this._options.gasLimit
}
const txResponse = await this._contractMethod(method, [credentialId], overrides)
return txResponse.hash
}
public getVcStatus = async (issuer: string, credentialId: string): Promise<string> => {
return this._contract.getVcStatus(issuer, credentialId)
}
// Isolate external function for sinon stub
private _contractMethod = async (method: string, parameters: any[], overrides: object): Promise<ethers.providers.TransactionResponse> => {
return this._contract[method](...parameters, overrides)
}
private initiateStatusEventSubscriber (eventType: PastEventType) {
const eventId = (eventType === PastEventType.set) ? 'VcStatusSet(address,address)' : 'VcStatusRemoved(address,address)'
const statusSetFilter = {
address: this.contractAddress,
topics: [ethers.utils.id(eventId)]
}
this.provider.on(statusSetFilter, (result) => {
switch (eventType) {
case PastEventType.set: this._onSetVcStatus.next(result as ContractEventData); break
case PastEventType.remove: this._onRemoveVcStatus.next(result as ContractEventData); break
}
})
}
private initiateErrorEventSubscriber () {
this.provider.on('error', (error) => {
this._onError.next(error)
})
}
// Probably remove since they are convenience methods, should live in a separate class
public getBlockNumber = async (): Promise<number> => {
return this.provider.getBlockNumber()
}
private initiateNewBlockEventSubscriber () {
this.provider.on('block', (blockNumber) => {
this._onNewBlock.next({ blockNumber } as NewBlockData)
})
}
public getPastStatusEvents (eventType: PastEventType, did: string, fromBlock = 0, toBlock: number | string = 'latest'): Promise<Array<ContractEventData>> {
const eventId: string = (eventType === PastEventType.set) ? 'VcStatusSet(address,address)' : 'VcStatusRemoved(address,address)'
const filter = {
address: this.contractAddress,
fromBlock: fromBlock,
toBlock: toBlock,
// second argument is an empty array to ignore issuer did
topics: [ethers.utils.id(eventId), [], did]
}
return this.provider.getLogs(filter) as Promise<Array<ContractEventData>>
}
}