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

refactor sdk to be better usable #1

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
node_modules
dist
src/graphql/types.ts
*.sw[a-z]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"start": "tsdx watch",
"build": "rm -rf dist && yarn tsc",
"test": "tsdx test --passWithNoTests",
"lint": "tsdx lint",
"lint": "tsdx lint src --fix",
"typecheck": "tsc --noEmit",
"prepare": "yarn generate",
"prepublishOnly": "yarn build",
Expand Down
284 changes: 284 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import { providers, utils } from 'ethers';
import { contracts } from '.';
import { QuestChainCommons } from './contracts/v1/contracts/QuestChain';
import {
getQuestChainInfo,
GlobalInfoFragment,
isSupportedNetwork,
QuestChainInfoFragment,
NetworkId,
} from './graphql';
import { waitUntilSubgraphIndexed } from './helpers';
import EventEmitter from 'events';

export enum QuestChainRole {
OWNER = '0x0000000000000000000000000000000000000000000000000000000000000000',
ADMIN = '0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775',
EDITOR = '0x21d1167972f621f75904fb065136bc8b53c7ba1c60ccd3a7758fbee465851e9c',
REVIEWER = '0xc10c77be35aff266144ed64c26a1fa104bae2f284ae99ac4a34203454704a185',
}

export class QuestChainsClient extends EventEmitter {
private chainId: NetworkId;
private provider: providers.Web3Provider;
private globalInfo: Record<NetworkId, GlobalInfoFragment> | null = null;

constructor(chainId: NetworkId, provider: providers.Web3Provider) {
if (!isSupportedNetwork(chainId)) {
throw new Error('Unsupported network');
}
super();
this.chainId = chainId;
this.provider = provider;
}

getNetworkId(): string {
return this.chainId;
}

async getGlobalInfo(): Promise<Record<NetworkId, GlobalInfoFragment>> {
if (this.globalInfo) return this.globalInfo;

this.globalInfo = await this.getGlobalInfo();

if (!this.globalInfo) {
throw new Error('Could not get global info');
}
return this.globalInfo;
}

async getChainInfo(chainId = this.chainId): Promise<GlobalInfoFragment> {
const globalInfo = await this.getGlobalInfo();
if (globalInfo && globalInfo[chainId]) {
return globalInfo[chainId];
}
throw new Error('Could not get chain info');
}

async getQuestChain(chainAddress: string, chainId = this.chainId): Promise<QuestChainInfoFragment | null> {
if (!utils.isAddress(chainAddress)) {
throw new Error('Invalid quest chain address');
}
return getQuestChainInfo(chainId, chainAddress);
}

private async handleTx(
tx: providers.TransactionResponse,
chainId = this.chainId,
): Promise<providers.TransactionReceipt> {
this.emit('txResponse', tx);

const receipt = await tx.wait();

this.emit('txReceipt', receipt);

const indexed = await waitUntilSubgraphIndexed(chainId, receipt.blockNumber);

this.emit('txIndexed', indexed);
return receipt;
}

async createQuestChain(
chainInfo: QuestChainCommons.QuestChainInfoStruct,
upgrade = false,
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
const { factoryAddress } = await this.getChainInfo(chainId);

const factoryContract: contracts.V1.QuestChainFactory = contracts.V1.QuestChainFactory__factory.connect(
factoryAddress,
provider.getSigner(),
);

let tx: providers.TransactionResponse;
if (upgrade) {
tx = await factoryContract.createAndUpgrade(chainInfo, utils.randomBytes(32));
} else {
tx = await factoryContract.create(chainInfo, utils.randomBytes(32));
}
const receipt = await this.handleTx(tx, chainId);

return receipt;
}

async upgradeQuestChain(
questChain: QuestChainInfoFragment,
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
if (questChain.premium) {
throw new Error('Quest chain is already upgraded');
}
const { factoryAddress } = await this.getChainInfo(chainId);

const factoryContract: contracts.V1.QuestChainFactory = contracts.V1.QuestChainFactory__factory.connect(
factoryAddress,
provider.getSigner(),
);

const tx: providers.TransactionResponse = await factoryContract.upgradeQuestChain(questChain.address);

const receipt = await this.handleTx(tx, chainId);

return receipt;
}

async grantRole(
questChain: QuestChainInfoFragment,
userAddress: string,
roleHash: QuestChainRole,
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
// role management is the same on all versions
const chainContract: contracts.V1.QuestChain = contracts.V1.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

const tx: providers.TransactionResponse = await chainContract.grantRole(roleHash, userAddress);
const receipt = await this.handleTx(tx, chainId);

return receipt;
}

async revokeRole(
questChain: QuestChainInfoFragment,
userAddress: string,
roleHash: QuestChainRole,
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
// role management is the same on all versions
const chainContract: contracts.V1.QuestChain = contracts.V1.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

const tx: providers.TransactionResponse = await chainContract.revokeRole(roleHash, userAddress);
const receipt = await this.handleTx(tx, chainId);

return receipt;
}

async pauseQuestChain(
questChain: QuestChainInfoFragment,
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
if (questChain.paused) {
throw new Error('Quest chain is already paused');
}
// pausing is the same on all versions
const chainContract: contracts.V1.QuestChain = contracts.V1.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

const tx: providers.TransactionResponse = await chainContract.pause();
const receipt = await this.handleTx(tx, chainId);

return receipt;
}

async unpauseQuestChain(
questChain: QuestChainInfoFragment,
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
if (questChain.paused) {
throw new Error('Quest chain is already paused');
}
// pausing is the same on all versions
const chainContract: contracts.V1.QuestChain = contracts.V1.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

const tx: providers.TransactionResponse = await chainContract.unpause();
const receipt = await this.handleTx(tx, chainId);

return receipt;
}

async addQuests(
questChain: QuestChainInfoFragment,
questDetailsList: string[],
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
if (questChain.paused) {
throw new Error('Quest chain is already paused');
}
if (questDetailsList.length === 0) {
throw new Error('No quests provided');
}
if (questChain.version === '0' && questDetailsList.length > 1) {
throw new Error('Adding multiple quests not supported on this quest chain version');
}

let tx: providers.TransactionResponse;
if (questChain.version === '0') {
const chainContract: contracts.V0.QuestChain = contracts.V0.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

tx = await chainContract.createQuest(questDetailsList[0]);
} else {
const chainContract: contracts.V1.QuestChain = contracts.V1.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

tx = await chainContract.createQuests(questDetailsList);
}

const receipt = await this.handleTx(tx, chainId);

return receipt;
}

async editQuests(
questChain: QuestChainInfoFragment,
questIdList: string[],
questDetailsList: string[],
chainId = this.chainId,
provider = this.provider,
): Promise<providers.TransactionReceipt> {
if (questChain.paused) {
throw new Error('Quest chain is already paused');
}
if (questDetailsList.length === 0) {
throw new Error('No quests provided');
}
if (questIdList.length !== questDetailsList.length) {
throw new Error('Quest details lengths do not match');
}
if (questChain.version === '0' && questDetailsList.length > 1) {
throw new Error('Editing multiple quests not supported on this quest chain version');
}

let tx: providers.TransactionResponse;
if (questChain.version === '0') {
const chainContract: contracts.V0.QuestChain = contracts.V0.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

tx = await chainContract.editQuest(questIdList[0], questDetailsList[0]);
} else {
const chainContract: contracts.V1.QuestChain = contracts.V1.QuestChain__factory.connect(
questChain.address,
provider.getSigner(),
);

tx = await chainContract.editQuests(questIdList, questDetailsList);
}

const receipt = await this.handleTx(tx, chainId);

return receipt;
}
}
2 changes: 1 addition & 1 deletion src/graphql/badgesForUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type UserBadges = {
imageUrl?: string | null | undefined;
questChain?: {
address: string | null | undefined;
}
};
}[];
chainId: string;
};
Expand Down
41 changes: 5 additions & 36 deletions src/graphql/client.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,10 @@
import { Client, createClient, dedupExchange, fetchExchange } from 'urql';

export type NetworkInfo = {
[chainId: string]: {
chainId: string;
subgraphName: string;
subgraphUrl: string;
};
};

export const SUPPORTED_NETWORK_INFO: NetworkInfo = {
'0x89': {
chainId: '0x89',
subgraphName: 'dan13ram/quest-chains-polygon',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-polygon',
},
'0x64': {
chainId: '0x64',
subgraphName: 'dan13ram/quest-chains-xdai',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-xdai',
},
'0x5': {
chainId: '0x5',
subgraphName: 'dan13ram/quest-chains-goerli',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-goerli',
},
'0x13881': {
chainId: '0x13881',
subgraphName: 'dan13ram/quest-chains-mumbai',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-mumbai',
},
};

export const SUPPORTED_NETWORKS = Object.keys(SUPPORTED_NETWORK_INFO);
import { NetworkIds, NetworkInfo } from '../networks';

const clients: Record<string, Client> = Object.values(SUPPORTED_NETWORK_INFO).reduce<Record<string, Client>>(
(o, info) => {
o[info.chainId] = createClient({
const clients: Record<string, Client> = Object.entries(NetworkInfo).reduce<Record<string, Client>>(
(o, [chainId, info]) => {
o[chainId] = createClient({
url: info.subgraphUrl,
exchanges: [dedupExchange, fetchExchange],
});
Expand All @@ -45,7 +14,7 @@ const clients: Record<string, Client> = Object.values(SUPPORTED_NETWORK_INFO).re
);

export const isSupportedNetwork = (chainId: string | undefined | null) =>
chainId ? SUPPORTED_NETWORKS.includes(chainId) : false;
chainId ? NetworkIds.includes(chainId) : false;

export const getClient = (chainId: string | undefined | null): Client => {
if (!chainId || !isSupportedNetwork(chainId)) {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import * as graphql from './graphql';
import * as contracts from './contracts';
import * as metadata from './metadata';
import * as helpers from './helpers';
import { QuestChainsClient, QuestChainRole } from './client';

export { graphql, contracts, metadata, helpers };
export { graphql, contracts, metadata, helpers, QuestChainsClient, QuestChainRole };
Loading