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

feat: add support for erc1155 tokens #192

Merged
merged 2 commits into from
Aug 18, 2023
Merged
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
157 changes: 157 additions & 0 deletions packages/sysweb3-keyring/src/transactions/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
createContractUsingAbi,
getErc20Abi,
getErc21Abi,
getErc55Abi,
} from '@pollum-io/sysweb3-utils';

export class EthereumTransactions implements IEthereumTransactions {
Expand Down Expand Up @@ -932,6 +933,162 @@ export class EthereumTransactions implements IEthereumTransactions {
}
};

sendSignedErc1155Transaction = async ({
receiver,
tokenAddress,
tokenId,
isLegacy,
maxPriorityFeePerGas,
maxFeePerGas,
gasPrice,
gasLimit,
}: ISendSignedErcTransactionProps): Promise<IResponseFromSendErcSignedTransaction> => {
const { decryptedPrivateKey } = this.getDecryptedPrivateKey();
const { accounts, activeAccountType, activeAccountId, activeNetwork } =
this.getState();
const { address: activeAccountAddress } =
accounts[activeAccountType][activeAccountId];

const sendERC1155Token = async () => {
const currentWallet = new ethers.Wallet(decryptedPrivateKey);
const walletSigned = currentWallet.connect(this.web3Provider);
let transferMethod;
try {
const _contract = new ethers.Contract(
tokenAddress,
getErc55Abi(),
walletSigned
);

if (isLegacy) {
transferMethod = await _contract.safeTransferFrom(
walletSigned.address,
receiver,
tokenId as number,
1,
[]
);
} else {
transferMethod = await _contract.safeTransferFrom(
walletSigned.address,
receiver,
tokenId as number,
1,
[]
);
}

return transferMethod;
} catch (error) {
throw error;
}
};

const sendERC1155TokenOnTrezor = async () => {
const signer = this.web3Provider.getSigner(activeAccountAddress);
const transactionNonce = await this.getRecommendedNonce(
activeAccountAddress
);
try {
const _contract = new ethers.Contract(
tokenAddress,
getErc55Abi(),
signer
);
const txData = _contract.interface.encodeFunctionData(
'safeTransferFrom',
[activeAccountAddress, receiver, tokenId, 1, []]
);
let txToBeSignedByTrezor;
if (isLegacy) {
txToBeSignedByTrezor = {
to: tokenAddress,
value: '0x0',
// @ts-ignore
gasLimit: `${gasLimit.toHexString()}`,
// @ts-ignore
gasPrice: `${gasPrice}`,
nonce: this.toBigNumber(transactionNonce)._hex,
chainId: activeNetwork.chainId,
data: txData,
};
} else {
txToBeSignedByTrezor = {
to: tokenAddress,
value: '0x0',
// @ts-ignore
gasLimit: `${gasLimit.toHexString()}`,
// @ts-ignore
maxFeePerGas: `${maxFeePerGas.toHexString()}`,
// @ts-ignore
maxPriorityFeePerGas: `${maxPriorityFeePerGas.toHexString()}`,
nonce: this.toBigNumber(transactionNonce)._hex,
chainId: activeNetwork.chainId,
data: txData,
};
}

const signature = await this.trezorSigner.signEthTransaction({
index: `${activeAccountId}`,
tx: txToBeSignedByTrezor,
});

if (signature.success) {
try {
let txFormattedForEthers;
if (isLegacy) {
txFormattedForEthers = {
to: tokenAddress,
value: '0x0',
gasLimit,
gasPrice,
data: txData,
nonce: transactionNonce,
chainId: activeNetwork.chainId,
type: 0,
};
} else {
txFormattedForEthers = {
to: tokenAddress,
value: '0x0',
gasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
data: txData,
nonce: transactionNonce,
chainId: activeNetwork.chainId,
type: 2,
};
}
signature.payload.v = parseInt(signature.payload.v, 16); //v parameter must be a number by ethers standards
const signedTx = ethers.utils.serializeTransaction(
txFormattedForEthers,
signature.payload
);
const finalTx = await this.web3Provider.sendTransaction(signedTx);

return finalTx as any;
} catch (error) {
console.log({ error });
throw error;
}
} else {
throw new Error(`Transaction Signature Failed. Error: ${signature}`);
}
} catch (error) {
console.log({ error });
throw error;
}
};

switch (activeAccountType) {
case KeyringAccountType.Trezor:
return await sendERC1155TokenOnTrezor();
default:
return await sendERC1155Token();
}
};

getRecommendedNonce = async (address: string) => {
try {
return await this.web3Provider.getTransactionCount(address, 'pending');
Expand Down
11 changes: 11 additions & 0 deletions packages/sysweb3-keyring/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ export interface IEthereumTransactions {
tokenId,
}: ISendSignedErcTransactionProps) => Promise<IResponseFromSendErcSignedTransaction>;

sendSignedErc1155Transaction: ({
receiver,
tokenAddress,
tokenId,
isLegacy,
gasPrice,
gasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
}: ISendSignedErcTransactionProps) => Promise<IResponseFromSendErcSignedTransaction>;

getBalance: (address: string) => Promise<number>;
getErc20TokensByAddress?: (
address: string,
Expand Down
126 changes: 64 additions & 62 deletions packages/sysweb3-utils/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { ethers as ethersModule } from 'ethers';
import sys from 'syscoinjs-lib';

import { createContractUsingAbi } from '.';
import ABI1155 from './abi/erc1155.json';
import abi20 from './abi/erc20.json';
import ABI721 from './abi/erc721.json';
// import ABI1155 from './abi/erc1155.json'
import tokens from './tokens.json';

import type {
Expand Down Expand Up @@ -173,6 +173,46 @@ export const fetchBalanceOfERC721Contract = async (
return fetchBalanceOfValue;
};

export const fetchBalanceOfERC1155Contract = async (
contractAddress: string,
address: string,
config: EthersFetcherConfigEthersLoaded,
tokenId: number
): Promise<number | undefined> => {
const contract = new config.ethers.Contract(
contractAddress,
ABI1155,
config.provider
) as NftContract;

const fetchBalanceOfValue = await contract.balanceOf(address, tokenId);

return fetchBalanceOfValue;
};

export const getERC1155StandardBalance = async (
contractAddress: string,
address: string,
provider: JsonRpcProvider,
tokenId: number
) => {
try {
const config = { provider, ethers: ethersModule };
const loaded = await loadEthers(config);

return await fetchBalanceOfERC1155Contract(
contractAddress,
address,
loaded,
tokenId
);
} catch (error) {
throw new Error(
`Verify current network or the contract address. Set the same network of token contract. Error: ${error}`
);
}
};

export const getERC721StandardBalance = async (
contractAddress: string,
address: string,
Expand All @@ -192,28 +232,22 @@ export const getERC721StandardBalance = async (

export const fetchStandardNftContractData = async (
contractAddress: Address,
tokenId: string,
config: EthersFetcherConfigEthersLoaded
): Promise<NftMetadata> => {
const contract = new config.ethers.Contract(
contractAddress,
ABI,
ABI1155,
config.provider
) as NftContract;

const [metadataUrl, owner] = await Promise.all([
url(contract, tokenId),
contract.ownerOf(tokenId).catch(() => ''),
const [name, symbol] = await Promise.all([
contract.name(),
contract.symbol(),
]);

const metadata = await fetchMetadata(metadataUrl);
const imageType = urlExtensionType(metadata.image);

return {
...metadata,
imageType,
metadataUrl,
owner,
name,
symbol,
};
};

Expand Down Expand Up @@ -350,49 +384,6 @@ export const reversePromise = (promise: Promise<unknown>): Promise<unknown> =>
export const IMAGE_EXT_RE = /\.(?:png|svg|jpg|jepg|gif|webp|jxl|avif)$/;
export const VIDEO_EXT_RE = /\.(?:mp4|mov|webm|ogv)$/;

// Guess a file type from the extension used in a URL
export const urlExtensionType = (url: string): NftMetadata['imageType'] => {
if (IMAGE_EXT_RE.test(url)) return 'image';
if (VIDEO_EXT_RE.test(url)) return 'video';

return 'unknown';
};

export const fetchMetadata = async (url: string): Promise<NftJsonMetadata> => {
const response = await fetch(url);

if (!response.ok) {
throw new Error(`Error when trying to request ${url}`);
}

let rawData;

try {
rawData = (await response.json()) as Record<string, unknown>;
} catch (error) {
rawData = { name: '', description: '', image: url };
}

let data = { ...rawData };

if (isNftMetadataMixedInJsonSchema(data)) {
data = fixNftMetadataMixedInJsonSchema(data);
}

data = fixIncorrectImageField(data);

if (!isNftMetadata(data)) {
throw new Error('Invalid data received');
}

return {
description: data.description || '',
image: data.image || '',
name: data.name || '',
rawData,
};
};

export const getTokenIconBySymbol = async (symbol: string): Promise<string> => {
symbol = symbol.toUpperCase();
const searchResults = await getSearch(symbol);
Expand Down Expand Up @@ -458,6 +449,22 @@ export const getTokenStandardMetadata = async (
}
};

export const getNftStandardMetadata = async (
contractAddress: string,
provider: JsonRpcProvider
) => {
try {
const config = { provider, ethers: ethersModule };
const loaded = await loadEthers(config);

return await fetchStandardNftContractData(contractAddress, loaded);
} catch (error) {
throw new Error(
`Verify current network. Set the same network of NFT token contract. Error: ${error}`
);
}
};

/**
* Converts a token to a fiat value
*
Expand Down Expand Up @@ -764,13 +771,8 @@ export interface IEtherscanNFT {
}

export interface NftMetadata {
description: string;
image: string;
imageType: 'image' | 'video' | 'unknown';
metadataUrl: string;
name: string;
owner: Address;
rawData: Record<string, unknown> | null;
symbol: string;
}
export type IErc20Token = {
name: string;
Expand Down