@foreverrbum/ethsign
Version:
This package will allow you to electronically sign documents within your application
693 lines (631 loc) • 21.1 kB
JavaScript
import { getCurrentBlockNumber, getIpfsHashFromBytes32, safeToAscii, timer } from "./dashboard";
import { isFileWithPassword } from './download';
import moment from 'moment';
import { getChainData,
getChainDataWithoutSigNum,
getChainDataWithSigNum,
merAddressData,
mergeDocumentsData,
mergeSignatureData,
getSingleChainDataWithouNum,
mergeSingleChainDocumentData,
mergeSingleChainSignatureData
} from "../components/dataVis/utils";
import { getTimestampOfFutureBlock } from "./block";
export const ENDPOINT_PREFIX = 'https://api.thegraph.com/subgraphs/name/ethsign/ethsign-3-';
export const API_VERSION = '';
export const USER_JOINED_QUERY = "User joined query";
export const SIGNATURE_SIGNED_QUERY = 'Signature signed query';
const getNetworkEndpoint = (chainId) => {
let endpoint = ENDPOINT_PREFIX;
switch(chainId) {
case 1:
endpoint += 'ethereum'; //'mainnet';
break;
case 3:
endpoint += 'ropsten';
break;
case 56:
endpoint += 'bsc';
break;
case 43114:
endpoint += 'avalanche';
break;
case 137:
endpoint += 'polygon';
break;
case 1287:
endpoint += 'moonbeam';
break;
case 250:
endpoint += 'fantom';
break;
default:
throw new Error("Unsupported Network for GraphQL Query with Chain ID " + chainId);
}
endpoint += API_VERSION;
return endpoint;
}
export const getDeploymentId = (chainId) => {
switch(chainId) {
case 1:
return 'QmaoPWdPLK815q2ygFbWTE339HRQfpvPVzAbg5Yjj72o4B';
case 3:
return 'QmUu2yAmfD6EwUP27PFwNkzjXGVnfxZkXkf21XfgN1X3Sm';
case 56:
return 'QmQxmQcWvpsw8V3BPEqGAQe3NRqvVaJREpyAYzBw7gA3mk';
case 43114:
return 'QmapYZcJu7GWNUg3Uf2vck37PRDPPmiZv3qZDQzs5x35v6';
case 137:
return 'QmecH6azJ2x5aSns8NRJVuQyGdgw9F94SiDK3wm3zw6Wi2';
case 250:
return 'QmdLz9Wd1dUaFVTT7tFQ8JwMPzLK1RjBuk4iohVqCcwUzZ';
default:
throw new Error("Unsupported Network for GraphQL Query with Chain ID " + chainId);
}
}
export const getContractHistoryQuery = (contractId) => {
return `
{
events(where: {contract: "${contractId}"}, orderBy: blockNumber, orderDirection: asc, first: 1000) {
blockNumber
timestamp
type
provider
storage_id0
storage_id1
involvedParty
number
}
}
`;
}
export const getBlockIndexStatusQuery = (chainId) => {
return `
{
indexingStatuses(subgraphs: ["${getDeploymentId(chainId)}"]) {
chains {
chainHeadBlock {
number
}
earliestBlock {
number
}
latestBlock {
number
}
}
}
}
`;
}
export const getSignatureValidityQuery = (docKey, signerAddress) => {
return `
{
contracts(where: {id: "${docKey}"}) {
name
currentSignedSignersList(where: {id: "${signerAddress}"}) {
id
}
}
events(where: {contract: "${docKey}", type: LogSignedDocument, involvedParty: "${signerAddress}"}) {
timestamp
}
}
`;
}
export const getSignatureExistenceQuery = (docKey, signerAddress) => {
return `
{
contracts(where: {id: "${docKey}"}) {
name
currentSignersList(where: {id: "${signerAddress}"}) {
id
}
}
}
`;
}
export const getContractsGivenAddressQuery = (address) => {
return `
{
users(where: {id: "${address}"}) {
contracts(orderBy: birth, orderDirection: desc) {
type
signed
contract {
id
status
}
}
}
}
`;
}
export const getContractDetailsGivenDocKeyQuery = (documentKey) => {
return `
{
contracts(where: { id: "${documentKey}" }) {
initiator {
id
}
name
birth
expiration
docStorage {
provider
storage_id0
storage_id1
}
metaStorage {
provider
storage_id0
storage_id1
}
totalSigners
currentSigners
currentSignedSigners
currentSignedSignersList {
id
}
status
events {
type
}
commentStorageList {
commentAuthor
provider
storage_id0
storage_id1
}
}
}
`;
}
export const getAllContractDataHistoryData = () => {
return `
{
infos(first: 1) {
totalDocumentsSigned
totalSignaturesSigned
}
}
`
}
export const getDocumentSignedQuery = (first = 100 , skip = 0) => {
return `
{
contracts(where: {consensusTimestamp_lt: ${moment().format('X')}}, first: ${first}, skip: ${skip}) {
name
id
consensusTimestamp
}
}
`
}
export const getSignatureSignedQuery = (first = 100 , skip = 0) => {
return `
{
events(where: {timestamp_lt: ${moment().format('x')}, type: LogSignedDocument}, first: ${first}, skip: ${skip}) {
type
timestamp
involvedParty
}
}
`
}
export const getAddressQuery = (first = 100 , skip = 0) => {
return `
{
users(where: {joinedTimestamp_lt: ${moment().format('X')}}, first: ${first}, skip: ${skip}) {
id
joinedTimestamp
}
}
`
}
export const getContractSignatureData = (documentKey) => {
return `
{
contracts(where: {id: "${documentKey}"}) {
initiator {
id
}
id
name
birth
signatureInfo {
signer {
id
}
signatureFieldsSigned
}
}
}
`
}
export const loadData = async (chainId, documentKey) => {
let endpoint = getNetworkEndpoint(chainId);
// Check to see if it's a valid chainId
if(endpoint.length == 0) {
return [];
}
let res;
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getContractHistoryQuery(documentKey),
}),
}).then((res) => res.json()).then((result) => {
if(!result.data.events) {
res = {};
return;
}
const newData = [];
let initiator = null;
result?.data.events.forEach((item) => {
const {type, timestamp, involvedParty, blockNumber, provider, storage_id0, storage_id1, number} = item;
switch (type){
case "LogNewDocument":
initiator = involvedParty;
newData.push({
type,
timestamp,
blockNumber,
initiator: involvedParty,
actionString: 0
});
break;
case "LogChangedDocumentStorage":
newData.push({
type,
timestamp,
blockNumber,
provider,
storage_id0,
storage_id1,
actionString: 1
});
break;
case "LogAddedNewSignerForDocument":
newData.push({
type,
timestamp,
blockNumber,
initiator: involvedParty,
actionString: 2
});
break;
case "LogSetNumberOfSignatureFields":
newData.push({
type,
timestamp,
blockNumber,
initiator: initiator,
signer: involvedParty,
number,
actionString: 3
});
break;
case "LogSignedDocument":
newData.push({
type,
timestamp,
blockNumber,
initiator: involvedParty,
actionString: 4
})
break;
}
})
res = newData;
});
return res;
}
export const loadContractDetails = async (chainId, documentKey, provider, address, signal) => {
let endpoint = getNetworkEndpoint(chainId);
// Check to see if it's a valid chainId
if(endpoint.length == 0) {
return [];
}
let res = {};
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signal:signal,
body: JSON.stringify({
query: getContractDetailsGivenDocKeyQuery(documentKey),
}),
}).then((res) => res.json()).then(async (result) => {
if(!(result?.data?.contracts?.length > 0)) {
res = null;
return;
}
let created_at = null;
if(provider) created_at = await provider.getBlock(parseInt(result?.data.contracts[0].birth));
let expire_at = null, expiration_block = parseInt(result?.data.contracts[0].expiration);
if (parseInt(result?.data.contracts[0].expiration) !== 0) {
let estimateDate = null;
if(provider) estimateDate = await getTimestampOfFutureBlock(parseInt(result?.data.contracts[0].expiration), await getCurrentBlockNumber(provider), provider)
expire_at = estimateDate ? moment(estimateDate).unix() : null
} else {
expiration_block = 'Never'
}
/*
* Statuses:
* 0: 'PDF Not Uploaded'
* 1: 'More Signers Needed'
* 2: 'Pending Signatures'
* 3: 'All Signed'
* 4: 'Waiting For Others'
*/
let status = parseInt(result?.data.contracts[0].status);
if(status == 2) {
let userHasSigned = false;
for(let signer of result?.data.contracts[0].currentSignedSignersList) {
if(signer.id === address?.toLowerCase()) {
userHasSigned = true;
}
}
if(userHasSigned) {
status = 4;
}
}
const docStorageProvider = result?.data.contracts[0].docStorage.provider
const docStorage_id0 = result?.data.contracts[0].docStorage.storage_id0
const docStorage_id1 = result?.data.contracts[0].docStorage.storage_id1
// const metaStorageProvider = result?.data.contracts[0].metaStorage.provider
const metaStorage_id0 = result?.data.contracts[0].metaStorage.storage_id0
const metaStorage_id1 = result?.data.contracts[0].metaStorage.storage_id1
let docHash = getIpfsHashFromBytes32(docStorage_id0) + getIpfsHashFromBytes32(docStorage_id1);
let metaHash = getIpfsHashFromBytes32(metaStorage_id0) + getIpfsHashFromBytes32(metaStorage_id1);
let withPassword = await isFileWithPassword(docHash);
let commentData = [];
let commentStorageList = result?.data.contracts[0].commentStorageList;
if(commentStorageList) {
commentStorageList.map((data) => {
let commentHash = getIpfsHashFromBytes32(data.storage_id0) + getIpfsHashFromBytes32(data.storage_id1);
if(commentHash != '') {
commentData.push({
address: data.commentAuthor.toLowerCase(),
provider: safeToAscii(data.provider),
commentHash: commentHash
});
}
});
}
// with the assumption that all files (doc, meta, comments) are stored at the same storageprovider
const storageProvider = safeToAscii(docStorageProvider);
let {signatureData, signers} = await loadContractSignatureDetails(chainId, documentKey, signal);
const {totalSigners, currentSigners} = result?.data.contracts[0]
if(totalSigners != currentSigners){
status = 1;
}
// initiator = _.signers.filter()
res.documentKey = documentKey;
res.initiator = result?.data.contracts[0].initiator.id.toLowerCase();
res.name = safeToAscii(result?.data.contracts[0].name).replace(/\0.*$/g, '');
res.creation = { block: parseInt(result?.data.contracts[0].birth), date: created_at.timestamp };
res.expiration = { block: expiration_block, date: expire_at ? expire_at : null };
res.status = status;
res.ipfsHash = docHash;
res.metaHash = metaHash;
res.storageProvider = storageProvider;
res.commentData = commentData;
res.withPassword = withPassword;
res.signers = signers;
res.signatureData = signatureData;
}).catch((err) => {
console.log(err);
});
return res;
}
export const loadContractSignatureDetails = async (chainId, documentKey, signal) => {
let endpoint = getNetworkEndpoint(chainId);
// Check to see if it's a valid chainId
if(endpoint.length == 0) {
return {signatureData: [], signers: []};
}
let signatureData = [];
let signers = [];
await fetch(endpoint, {
method: 'POST',
signal: signal,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getContractSignatureData(documentKey),
}),
}).then((res) => res.json()).then(async (result) => {
if(result?.data.contracts[0]?.signatureInfo?.length > 0) {
result?.data.contracts[0].signatureInfo.map((sigInfo) => {
const signed = sigInfo.signatureFieldsSigned.filter(field => field == true);
const numSignedFields = signed.length;
const numTotalFields = sigInfo.signatureFieldsSigned.length;
signatureData.push({
signer: sigInfo.signer.id.toLowerCase(),
signed: signed.length,
notSigned: numTotalFields-numSignedFields,
fieldSigned: sigInfo.signatureFieldsSigned
})
signers.push({
address: sigInfo.signer.id.toLowerCase(),
avatar: null,
alias: null,
fullySigned: numSignedFields == numTotalFields
})
})
}
}).catch((err) => {
console.log(err);
});
return {signatureData: signatureData, signers: signers};
}
export const loadContracts = async (chainId, address, signal) => {
let endpoint = getNetworkEndpoint(chainId);
// Check to see if it's a valid chainId
if(endpoint.length == 0) {
return [];
}
let data = [];
await fetch(endpoint, {
method: 'POST',
signal: signal,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getContractsGivenAddressQuery(address.toLowerCase()),
}),
}).then((res) => res.json()).then(async (result) => {
if(result?.data?.users[0]) {
data = result.data.users[0].contracts;
for(let c of data) {
c.ethAccount = address.toLowerCase();
}
// mine_data = result.data.users[0].createdByMe;
// archived_data = result.data.users[0].archived;
// shared_data = result.data.users[0].sharedWithMe;
}
}).catch((err) => {
console.log(err);
});
return data;
}
export const getChainIndexStatus = async (chainId) => {
let endpoint = 'https://api.thegraph.com/index-node/graphql';
let data = [];
let blocksBehind = 0;
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getBlockIndexStatusQuery(chainId),
}),
}).then((res) => res.json()).then(async (result) => {
if(result?.data?.indexingStatuses[0]) {
data = result.data.indexingStatuses[0].chains[0];
blocksBehind = data.chainHeadBlock.number - data.latestBlock.number;
}
});
return blocksBehind;
}
export const validateSignature = async (chainId, docKey, signerAddress) => {
// STATUS:
// 0 - Does not exist
// 1 - Signature Verified
// 2 - Signature Pending
let endpoint = getNetworkEndpoint(chainId);
// Check to see if it's a valid chainId
if(endpoint.length == 0) {
return {
status: 0,
networkId: chainId,
doc: null,
signerDetails: null,
};
}
let ret = null;
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getSignatureValidityQuery(docKey, signerAddress.toLowerCase()),
}),
}).then((res) => res.json()).then(async (result) => {
if(result.data?.contracts?.length > 0 && result.data?.events?.length > 0) {
// Valid signature
ret = {
status: 1,
networkId: chainId,
doc: {
name: safeToAscii(result.data.contracts[0].name).replace(/\0.*$/g, ''),
key: docKey,
},
signerDetails: {
address: signerAddress,
timestamp: result.data.events[0].timestamp,
}
}
} else {
// Not a valid signature
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getSignatureExistenceQuery(docKey, signerAddress.toLowerCase()),
}),
}).then((res) => res.json()).then((result) => {
if(result.data?.contracts?.length > 0 && result.data?.contracts[0].currentSignersList?.length > 0) {
ret = {
status: 2,
networkId: chainId,
doc: {
name: safeToAscii(result.data.contracts[0].name).replace(/\0.*$/g, ''),
key: docKey,
},
signerDetails: {
address: signerAddress,
timestamp: null,
},
}
} else {
ret = {
status: 0,
networkId: chainId,
doc: null,
signerDetails: null,
};
}
})
}
});
return ret;
}
// the network should be ['mainnet', 'ropsten', 'bsc', 'avalanche', 'polygon', 'moonbeam','fantom'] , but in the 'moonbeam' have some bug, we can not get the right data
export const network = ['ethereum', 'ropsten', 'bsc', 'avalanche', 'polygon', 'fantom'];
export const loadAllContractHistoryData = async () => {
return await getChainData(ENDPOINT_PREFIX, "", getAllContractDataHistoryData());
}
export const loadAllDocumentsSignedData = async (documentsSignedNumOnEachChain = []) => {
return mergeDocumentsData(await getChainDataWithSigNum(documentsSignedNumOnEachChain, getDocumentSignedQuery, USER_JOINED_QUERY));
}
export const loadAllSignatureSignedData = async (signaturesSignedNumOnEachChain) => {
return mergeSignatureData(await getChainDataWithSigNum(signaturesSignedNumOnEachChain, getSignatureSignedQuery, USER_JOINED_QUERY));
}
export const loadAllAddressCount = async () => {
return merAddressData(await getChainDataWithoutSigNum(network, getAddressQuery, USER_JOINED_QUERY));
}
export const loadDocumentsSignedByNetwork = async (networkName) => {
return mergeSingleChainDocumentData(await getSingleChainDataWithouNum(networkName, getDocumentSignedQuery, USER_JOINED_QUERY));
}
export const loadSignatureSignedByNetwork = async (networkName) => {
return mergeSingleChainSignatureData(await getSingleChainDataWithouNum(networkName, getSignatureSignedQuery, USER_JOINED_QUERY));
}
export const fetchChainData = async (endpoint, networkName, query) => {
return await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query
}),
})
.then(res=> res.json())
.then(({data}) => {
return {networkName, data};
})
}