cic-client
Version:
Typescript libraries for building CIC client applications
200 lines (173 loc) • 6.73 kB
text/typescript
import {DeclaratorHelper} from "./helper";
const LRU = require('lru-cache');
import { abi, interfaceCodes } from './solidity';
import { zeroAddress } from './const';
import { EVMMethodID, EVMAddress, EVMContract, FungibleToken } from './typ';
import { subtle } from './crypto';
import { bytesToHex } from './digest';
import { FileGetter } from './file';
class KV {
store = {};
public put(k:string, v:any) {
this.store[k] = v;
}
public get(k:string): any {
return this.store[k];
}
public peek(k:string): any {
return this.store[k];
}
}
interface Registry {
getAbi(abiName:string): Promise<object>;
getContract(address:EVMAddress): Promise<EVMContract>;
getContractByName(contractName:string, abiName?:string, requireInterfaces?:EVMMethodID[]): Promise<EVMContract>;
getContractAddressByName(contractName:string, abiName?:string, requireInterfaces?:EVMMethodID[]): Promise<string>;
getToken(address:EVMAddress): Promise<FungibleToken>;
getTokenBySymbol(tokenRegistryContractName:string, symbol:string, checkInterface:boolean): Promise<FungibleToken>;
addToken(address:EVMAddress): Promise<EVMContract>;
declaratorHelper;
}
class CICRegistry {
w3: any
fileGetter: FileGetter
address: string
abiName: string;
contract: any // TODO: replace with web3 contract type
store: KV
paths: string[]
onload: (a:string) => void
declaratorHelper: DeclaratorHelper
constructor(w3:any, address:EVMAddress, abiName: string, fileGetter:FileGetter, paths?:string[]) {
this.w3 = w3;
this.address = address;
this.abiName = abiName;
this.store = new KV();
this.paths = paths;
this.fileGetter = fileGetter;
this.declaratorHelper = new DeclaratorHelper(w3, this);
}
public async load() {
const registryAbi = await abi(this.fileGetter, this.abiName, this.paths);
this.contract = new this.w3.eth.Contract(registryAbi, this.address);
const contractIdHex = this.w3.utils.toHex('ContractRegistry');
const contractId = this.w3.eth.abi.encodeParameter('bytes32', contractIdHex);
const confirmedContractAddress = await this.contract.methods.addressOf(contractId).call();
if (this.address.toLowerCase() != confirmedContractAddress.toLowerCase()) {
throw new Error('cic registry contract entry does not match its own address');
}
if (this.onload !== undefined) {
this.onload(this.address);
}
}
public async getAbi(abiName:string): Promise<object> {
let abiObject = this.store.get(toAbiKey(abiName))
if (abiObject === undefined) {
abiObject = await abi(this.fileGetter, abiName, this.paths);
this.store.put(abiName, abiObject);
}
return abiObject;
}
public async addToken(address:EVMAddress, requireTrust:boolean=false): Promise<EVMContract> {
if (requireTrust) {
throw new Error('trust check not implemented yet, sorry');
}
const erc20Abi = await this.getAbi('ERC20');
const tokenContract = new this.w3.eth.Contract(erc20Abi, address);
const tokenSymbol = await tokenContract.methods.symbol().call();
if (tokenSymbol === undefined) {
throw new Error('attempted to add token contract ' + address + ' which is not an ERC20 token');
}
this.store.put(toTokenKey(tokenSymbol), tokenContract);
this.store.put(address, tokenContract);
return tokenContract;
}
public async getContract(address:EVMAddress): Promise<EVMContract> {
let contract = this.store.get(address);
if (contract === undefined) {
throw new Error('unknown contract ' + address);
}
return contract;
}
public async getToken(address:EVMAddress): Promise<EVMContract> {
const tokenContract = await this.getContract(address);
const tokenSymbol = await tokenContract.methods.symbol().call();
if (tokenSymbol === undefined) {
throw new Error('contract ' + address + ' is not an ERC20 token');
}
return tokenContract;
}
public async getContractByName(contractName:string, abiName?:string, requireInterfaces?:EVMMethodID[]): Promise<EVMContract> {
const contractKey = toContractKey(contractName);
let contract = this.store.get(contractKey);
if (contract === undefined) {
const contractAddress = await this.getContractAddressByName(contractName, abiName, requireInterfaces);
if (abiName === undefined) {
abiName = contractName;
}
const contractAbi = await this.getAbi(abiName);
contract = new this.w3.eth.Contract(contractAbi, contractAddress);
this.store.put(contractKey, contract);
this.store.put(contractAddress, contract);
}
return contract;
}
public async getContractAddressByName(contractName:string, abiName?:string, requireInterfaces?:EVMMethodID[]): Promise<string> {
const contract_id_hex = this.w3.utils.toHex(contractName);
const contract_id = this.w3.eth.abi.encodeParameter('bytes32', contract_id_hex);
const contractAddress = await this.contract.methods.addressOf(contract_id).call();
if (contractAddress == zeroAddress) {
throw new Error('unknown contract ' + contractName + ' (' + contract_id_hex + ')');
}
return contractAddress;
}
public async getFungibleToken(tokenAddress:EVMAddress, checkInterface:boolean=false): Promise<FungibleToken> {
const tokenAbi = await this.getAbi('ERC20');
const tokenContract = new this.w3.eth.Contract(tokenAbi, tokenAddress);
if (checkInterface) {
if (!tokenContract.methods.supportsInterface('ERC20')) {
throw 'token does not declare ERC20 interface support';
}
}
const tokenSymbol = await tokenContract.methods.symbol().call();
this.store.put(toTokenKey(tokenSymbol), tokenContract);
this.store.put(tokenAddress, tokenContract);
return tokenContract;
}
public async getTokenBySymbol(tokenRegistryContractName:string, symbol:string, checkInterface:boolean=false): Promise<FungibleToken> {
let token = this.store.get(toTokenKey(symbol));
if (token !== undefined) { return token; }
const tokenRegistryContract = await this.getContractByName(
tokenRegistryContractName,
'Registry',
[interfaceCodes.Registry],
);
const symbolId = await toRegistryKey(symbol);
const tokenAddress = await tokenRegistryContract.methods.addressOf(symbolId).call();
if (tokenAddress === zeroAddress) {
throw 'unknown token "' + symbol + '" using registry "' + tokenRegistryContractName + '"';
}
token = this.getFungibleToken(tokenAddress, checkInterface);
return token;
}
}
async function toRegistryKey(s:string): Promise<string> {
const sDigest = await subtle.digest('SHA-256', s);
const sId = '0x' + bytesToHex(sDigest);
return sId;
}
function toContractKey(s:string): string {
return 'contract:' + s;
}
function toAbiKey(s:string): string {
return 'abi:' + s;
}
function toTokenKey(s:string): string {
return 'token:' + s;
}
export {
CICRegistry,
Registry,
toContractKey,
toRegistryKey
}