@colony/colony-js-contract-client
Version:
Method-like interface for Smart Contracts
250 lines (240 loc) • 6.98 kB
Flow
/* @flow */
/* eslint-disable import/no-cycle */
import bs58 from 'bs58';
import BigNumber from 'bn.js';
import {
isHexStrict,
hexToBytes,
hexToUtf8,
toHex,
utf8ToHex,
} from 'web3-utils';
import {
isValidAddress,
isBigNumber,
isEmptyHexString,
} from '@colony/colony-js-utils';
import type { ParamTypes, ParamTypeDef } from '../flowtypes';
import { isBoolean } from './paramValidation';
const passThrough = value => value;
/**
* A map of ParamTypes to ParamTypeDefs
* For each ParamType (e.g. address), defines how a value should be validated
* or converted for input/output.
*/
const PARAM_TYPE_MAP: {
[type: ParamTypes]: ParamTypeDef,
} = {
'[address]': {
validate(value: any) {
if (!Array.isArray(value)) return false;
return value.every(element => isValidAddress(element));
},
convertOutput(value) {
if (!Array.isArray(value)) return [];
return value.map(element => (isValidAddress(element) ? element : null));
},
convertInput: passThrough,
},
'[bigNumber]': {
validate(value: any) {
if (!Array.isArray(value)) return false;
return value.every(element => isBigNumber(element));
},
convertOutput(value) {
if (!Array.isArray(value)) return [];
return value.map(element => (isBigNumber(element) ? element : null));
},
convertInput: passThrough,
},
'[hexString]': {
validate(value: any) {
if (!Array.isArray(value)) return false;
return value.every(element => isHexStrict(element));
},
convertOutput(value: any) {
if (!Array.isArray(value)) return [];
return value.map(element => toHex(element));
},
convertInput: passThrough,
},
'[number]': {
validate(value: any) {
if (!Array.isArray(value)) return false;
return value.every(element => Number.isFinite(element));
},
convertOutput(value) {
if (!Array.isArray(value)) return [];
return value.map(element => {
// convert BN (ethers or bn.js)
const number = isBigNumber(element) ? element.toNumber() : element;
return Number.isFinite(number) ? number : null;
});
},
convertInput: passThrough,
},
address: {
validate: isValidAddress,
convertOutput(value) {
return isValidAddress(value) ? value : null;
},
convertInput: passThrough,
},
// Either a valid address, or an empty address
anyAddress: {
validate(value: any) {
return isValidAddress(value) || isEmptyHexString(value);
},
convertInput(value: string) {
// Expand `0x0` to a full-length address
return value.padEnd(42, '0');
},
convertOutput(value: any) {
// Expand `0x0` to a full-length address (for a valid address)
return isValidAddress(value) || isEmptyHexString(value)
? value.padEnd(42, '0')
: null;
},
},
bigNumber: {
validate: isBigNumber,
convertOutput(value: any) {
if (isBigNumber(value)) {
return value;
}
if (Number.isFinite(value)) {
return new BigNumber(value);
}
return null;
},
convertInput: passThrough,
},
boolean: {
validate: isBoolean,
convertOutput(value) {
return isBoolean(value) ? value : null;
},
convertInput: passThrough,
},
bytes32String: {
validate(value: any) {
// Must be a string and will fit into bytes32
return typeof value === 'string' && utf8ToHex(value).length <= 66;
},
convertOutput(value) {
// Will be returned as a hex string, we want the UTF-8 string
return hexToUtf8(value);
},
convertInput(value) {
// Adapter expects a 32 byte hex string, convert from UTF-8 and pad
const converted = utf8ToHex(value);
return `0x${converted.slice(2).padStart(64, '0')}`;
},
},
date: {
validate(value: any) {
// XXX This allows dates initialised without a value (or with `0` value)
return value instanceof Date;
},
convertOutput(value: any) {
const converted = parseInt(
isBigNumber(value) ? value.toNumber() : value,
10,
);
// Recreate the date by adding milliseconds to the timestamp
return converted > 0 ? new Date(converted * 1000) : null;
},
convertInput(value: Date) {
// Dates are stored as timestamps without milliseconds
return parseInt(value.setMilliseconds(0) / 1000, 10);
},
},
hexString: {
validate(value: any) {
return isHexStrict(value);
},
convertOutput(value: any) {
return toHex(value);
},
convertInput: passThrough,
},
number: {
validate(value: any) {
return Boolean(Number.isFinite(value));
},
convertOutput(value: any) {
const converted = isBigNumber(value) ? value.toNumber() : value;
return Number.isFinite(converted) ? converted : null;
},
convertInput: passThrough,
},
string: {
validate(value: any) {
return typeof value === 'string';
},
convertOutput(value: any) {
if (isHexStrict(value)) {
if (isEmptyHexString(value)) return null;
try {
return hexToUtf8(value);
} catch (error) {
// ignore error: not a UTF8-encoded value
}
}
return typeof value === 'string' && value.length ? value : null;
},
convertInput: passThrough,
},
ipfsHash: {
validate(value: any) {
return (
typeof value === 'string' &&
value.slice(0, 2) === 'Qm' &&
value.length === 46
);
},
convertOutput(value: any) {
if (isHexStrict(value) && !isEmptyHexString(value)) {
const hex = `0x1220${value.slice(2)}`;
const bytes = hexToBytes(hex);
return bs58.encode(Buffer.from(bytes));
}
return null;
},
convertInput(value: string) {
return `0x${bs58
.decode(value)
.slice(2)
.toString('hex')}`;
},
},
};
/**
* Given the key of a ParamTypeDef (namely `validate`, `convertOutput` or
* `convertInput`), a value and ParamType, pluck the appropriate function
* from the type map and call it with the value.
*/
function typeMapFn(fnName: $Keys<ParamTypeDef>, value: any, type: ParamTypes) {
const {
[type]: { [fnName]: fn },
} = PARAM_TYPE_MAP;
return fn(value);
}
export function addParamType(type: string, def: ParamTypeDef): void {
Object.assign(PARAM_TYPE_MAP, { [type]: def });
}
/**
* Given a value and a ParamType, validate the value according to that type's
* `validate` function
*/
export const validateValueType = typeMapFn.bind(this, 'validate');
/**
* Given a value and a ParamType, convert the value according to that type's
* `convertOutput` function
*/
export const convertOutputValue = typeMapFn.bind(this, 'convertOutput');
/**
* Given a value and a ParamType, convert the value according to that type's
* `convertInput` function
*/
export const convertInputValue = typeMapFn.bind(this, 'convertInput');