@tatumio/celo-connector
Version:
Celo Connector for Tatum API
346 lines (308 loc) • 15.6 kB
text/typescript
import {PinoLogger} from 'nestjs-pino';
import axios from 'axios';
import {CeloProvider} from '@celo-tools/celo-ethers-wrapper';
import {CeloError} from './CeloError';
import {
BurnCeloErc20,
CeloBurnErc721,
CeloDeployErc721,
CeloMintErc721,
CeloMintMultipleErc721,
CeloSmartContractMethodInvocation,
CeloTransferErc721,
CEUR_ADDRESS_MAINNET,
CEUR_ADDRESS_TESTNET,
Currency,
CUSD_ADDRESS_MAINNET,
CUSD_ADDRESS_TESTNET,
DeployCeloErc20,
generateAddressFromXPub,
generatePrivateKeyFromMnemonic,
generateWallet,
MintCeloErc20,
prepareCeloBurnErc20SignedTransaction,
prepareCeloBurnErc721SignedTransaction,
prepareCeloDeployErc20SignedTransaction,
prepareCeloDeployErc721SignedTransaction,
prepareCeloMintErc20SignedTransaction,
prepareCeloMintErc721SignedTransaction,
prepareCeloMintMultipleErc721SignedTransaction,
prepareCeloOrCUsdSignedTransaction,
prepareCeloSmartContractWriteMethodInvocation,
prepareCeloTransferErc20SignedTransaction,
prepareCeloTransferErc721SignedTransaction,
sendCeloSmartContractReadMethodInvocationTransaction,
SmartContractReadMethodInvocation,
TransactionHash,
TransferCeloOrCeloErc20Token,
} from '@tatumio/tatum';
import erc721_abi from '@tatumio/tatum/dist/src/contracts/erc721/erc721_abi';
import token_abi from '@tatumio/tatum/dist/src/contracts/erc20/token_abi';
import {fromWei} from 'web3-utils';
import {Block, Transaction, TransactionReceipt} from 'web3-eth';
import Web3 from 'web3';
export abstract class CeloService {
protected constructor(protected readonly logger: PinoLogger) {
}
protected abstract storeKMSTransaction(txData: string, currency: string, signatureId: string[], index?: number): Promise<string>;
protected abstract completeKMSTransaction(txId: string, signatureId: string): Promise<void>;
protected abstract isTestnet(): Promise<boolean>;
protected abstract getNodesUrl(testnet: boolean): Promise<string[]>;
public async broadcast(txData: string, signatureId?: string, withdrawalId?: string): Promise<{
txId: string,
failed?: boolean,
}> {
this.logger.info(`Broadcast tx for CELO with data '${txData}'`);
let txId;
try {
const url = (await this.getNodesUrl(await this.isTestnet()))[0];
const {result, error} = (await axios.post(url, {
jsonrpc: '2.0',
id: 0,
method: 'eth_sendRawTransaction',
params: [txData]
}, {headers: {'Content-Type': 'application/json'}})).data;
if (error) {
throw new CeloError(`Unable to broadcast transaction due to ${error.message}.`, 'celo.broadcast.failed');
}
txId = result;
} catch (e) {
if (e.constructor.name === CeloError.name) {
throw e;
}
this.logger.error(e);
throw new CeloError(`Unable to broadcast transaction due to ${e.message}.`, 'celo.broadcast.failed');
}
if (signatureId) {
try {
await this.completeKMSTransaction(txId, signatureId);
} catch (e) {
this.logger.error(e);
return {txId, failed: true};
}
}
return {txId};
}
public async getCurrentBlock(testnet?: boolean): Promise<number> {
const t = testnet === undefined ? await this.isTestnet() : testnet;
return (await this.getClient(t)).eth.getBlockNumber();
}
public async getBalance(address: string, testnet?: boolean): Promise<{ celo: string, cUsd: string, cEur: string }> {
const t = testnet === undefined ? await this.isTestnet() : testnet;
const provider = new CeloProvider((await this.getNodesUrl(t))[0]);
// @ts-ignore
const cUsd = new ((await this.getClient(t))).eth.Contract(token_abi, t ? CUSD_ADDRESS_TESTNET : CUSD_ADDRESS_MAINNET);
// @ts-ignore
const cEur = new ((await this.getClient(t))).eth.Contract(token_abi, t ? CEUR_ADDRESS_TESTNET : CEUR_ADDRESS_MAINNET);
return {
celo: fromWei((await provider.getBalance(address)).toString(), 'ether'),
cUsd: fromWei(await cUsd.methods.balanceOf(address).call(), 'ether'),
cEur: fromWei(await cEur.methods.balanceOf(address).call(), 'ether'),
};
}
public async getBlock(hash: string | number, testnet?: boolean): Promise<Block> {
const t = testnet === undefined ? await this.isTestnet() : testnet;
return (await this.getClient(t)).eth.getBlock(hash, true);
}
public async getTransaction(txId: string, testnet?: boolean): Promise<Transaction & TransactionReceipt> {
const t = testnet === undefined ? await this.isTestnet() : testnet;
try {
const web3 = await this.getClient(t);
const {r, s, v, hash, ...transaction} = (await web3.eth.getTransaction(txId)) as any;
let receipt: TransactionReceipt = undefined;
try {
receipt = await web3.eth.getTransactionReceipt(hash);
} catch (_) {
transaction.transactionHash = hash;
}
return {...transaction, ...receipt};
} catch (e) {
this.logger.error(e);
throw new CeloError('Transaction not found. Possible not exists or is still pending.', 'tx.not.found');
}
}
public async transfer(body: TransferCeloOrCeloErc20Token): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloOrCUsdSignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async transferErc20(body: TransferCeloOrCeloErc20Token): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloTransferErc20SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async invokeSmartContractMethod(body: CeloSmartContractMethodInvocation | SmartContractReadMethodInvocation) {
const testnet = await this.isTestnet();
if (body.methodABI.stateMutability === 'view') {
return sendCeloSmartContractReadMethodInvocationTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
}
const txData = await prepareCeloSmartContractWriteMethodInvocation(testnet, body as CeloSmartContractMethodInvocation, (await this.getNodesUrl(testnet))[0]);
if ((body as CeloSmartContractMethodInvocation).signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [(body as CeloSmartContractMethodInvocation).signatureId], (body as CeloSmartContractMethodInvocation).index)};
} else {
return this.broadcast(txData);
}
}
public async mintErc20(body: MintCeloErc20): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloMintErc20SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async burnErc20(body: BurnCeloErc20): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloBurnErc20SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async deployErc20(body: DeployCeloErc20): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloDeployErc20SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async getErc20Balance(address: string, contractAddress: string) {
// @ts-ignore
const c = new ((await this.getClient(await this.isTestnet()))).eth.Contract(token_abi, contractAddress);
return {balance: await c.methods.balanceOf(address).call()};
}
// TODO: call any SC method
// TODO: store data / get data
// TODO: estimate gas
public async getTransactionCount(address: string, c?: Web3) {
const client = c || await this.getClient(await this.isTestnet());
return client.eth.getTransactionCount(address, 'pending');
}
public async getBalanceErc721(address: string, contractAddress: string): Promise<{ data: string }> {
// @ts-ignore
const c = new (await this.getClient(await this.isTestnet())).eth.Contract(erc721_abi, contractAddress);
try {
return {data: await c.methods.balanceOf(address).call()};
} catch (e) {
this.logger.error(e);
throw new CeloError(`Unable to obtain information for token. ${e}`, 'celo.erc721.failed');
}
}
public async getTokenErc721(address: string, index: number, contractAddress: string): Promise<{ data: string }> {
// @ts-ignore
const c = new (await this.getClient(await this.isTestnet())).eth.Contract(erc721_abi, contractAddress);
try {
return {data: await c.methods.tokenOfOwnerByIndex(address, index).call()};
} catch (e) {
this.logger.error(e);
throw new CeloError(`Unable to obtain information for token. ${e}`, 'celo.erc721.failed');
}
}
public async getMetadataErc721(token: string, contractAddress: string): Promise<{ data: string }> {
// @ts-ignore
const c = new (await this.getClient(await this.isTestnet())).eth.Contract(erc721_abi, contractAddress);
try {
return {data: await c.methods.tokenURI(token).call()};
} catch (e) {
this.logger.error(e);
throw new CeloError(`Unable to obtain information for token. ${e}`, 'celo.erc721.failed');
}
}
public async getOwnerErc721(token: string, contractAddress: string): Promise<{ data: string }> {
// @ts-ignore
const c = new (await this.getClient(await this.isTestnet())).eth.Contract(erc721_abi, contractAddress);
try {
return {data: await c.methods.ownerOf(token).call()};
} catch (e) {
this.logger.error(e);
throw new CeloError(`Unable to obtain information for token. ${e}`, 'celo.erc721.failed');
}
}
public async getTokensOfOwner(address: string, contractAddress: string): Promise<{ data: string }> {
// @ts-ignore
const c = new (await this.getClient(await this.isTestnet())).eth.Contract(erc721_abi, contractAddress);
try {
return {data: await c.methods.tokensOfOwner(address).call()};
} catch (e) {
this.logger.error(e);
throw new CeloError(`Unable to obtain information for token. ${e}`, 'celo.erc721.failed');
}
}
public async transferErc721(body: CeloTransferErc721): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloTransferErc721SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async mintErc721(body: CeloMintErc721): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloMintErc721SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async mintMultipleErc721(body: CeloMintMultipleErc721): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloMintMultipleErc721SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async burnErc721(body: CeloBurnErc721): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloBurnErc721SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async deployErc721(body: CeloDeployErc721): Promise<TransactionHash | { signatureId: string }> {
const testnet = await this.isTestnet();
const txData = await prepareCeloDeployErc721SignedTransaction(testnet, body, (await this.getNodesUrl(testnet))[0]);
if (body.signatureId) {
return {signatureId: await this.storeKMSTransaction(txData, Currency.CELO, [body.signatureId], body.index)};
} else {
return this.broadcast(txData);
}
}
public async web3Method(body: any) {
try {
return (await axios.post((await this.getNodesUrl(await this.isTestnet()))[0], body, {headers: {'Content-Type': 'application/json'}})).data;
} catch (e) {
this.logger.error(e);
throw e;
}
}
public async generateAddress(xpub: string, i: number) {
return {address: await generateAddressFromXPub(Currency.CELO, await this.isTestnet(), xpub, i)};
}
public async generateWallet(mnemonic?: string) {
return generateWallet(Currency.CELO, await this.isTestnet(), mnemonic);
}
public async generateAddressPrivateKey(index: number, mnemonic: string) {
return {key: await generatePrivateKeyFromMnemonic(Currency.CELO, await this.isTestnet(), mnemonic, index)};
}
protected async getClient(testnet: boolean): Promise<Web3> {
return new Web3((await this.getNodesUrl(testnet))[0]);
}
}