Skip to content

Commit

Permalink
Merge pull request #19 from ava-labs/dev
Browse files Browse the repository at this point in the history
v0.9.8
  • Loading branch information
kanatliemre authored Oct 21, 2021
2 parents 36696eb + e4466c9 commit d148f24
Show file tree
Hide file tree
Showing 40 changed files with 6,030 additions and 1,097 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# CHANGELOG

## v0.9.8

#### Added

- Mnemonic Wallets can be initialized as different accounts
- Network changes emit an event, wallets subscribe to these events
- Wallet `destroy` method to clean memory

#### Changed

- Switch to `isomorphic-dompurify`
- History module rewritten and refactored.

## v0.9.7

#### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@
"bip39": "^3.0.4",
"bn.js": "5.1.1",
"create-hash": "1.2.0",
"dompurify": "^2.3.1",
"ethereumjs-util": "^7.0.7",
"ethers": "^5.1.4",
"hdkey": "2.0.1",
"isomorphic-dompurify": "^0.15.0",
"moment": "^2.29.1",
"rollup-plugin-commonjs": "^10.1.0",
"sockette": "^2.0.6",
Expand Down
22 changes: 22 additions & 0 deletions scripts/walletAccountsMnemonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MnemonicWallet } from '../dist';

const mnemonic = `state usage height lumber ski federal silly axis train sustain pizza try embark giraffe motion account loud actress blood chapter blame network blossom hat`;
// or generate new mnemonic
// const mnemonic = MnemonicWallet.generateMnemonicPhrase()

let account0 = new MnemonicWallet(mnemonic);
let account1 = new MnemonicWallet(mnemonic, 1);
let account2 = new MnemonicWallet(mnemonic, 2);

// Account 0
console.log('Account 0');
console.log('X:', account0.getAddressX());
console.log('C:', account0.getAddressC());
// Account 1
console.log('Account 1');
console.log('X:', account1.getAddressX());
console.log('C:', account1.getAddressC());
// Account 2
console.log('Account 2');
console.log('X:', account2.getAddressX());
console.log('C:', account2.getAddressC());
2 changes: 1 addition & 1 deletion src/Asset/Assets.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { xChain } from '@/Network/network';

import { iAssetCache, iAssetDescriptionClean } from '@/Asset/types';
import DOMPurify from 'dompurify';
import DOMPurify from 'isomorphic-dompurify';

let assetCache: iAssetCache = {};

Expand Down
2 changes: 1 addition & 1 deletion src/Asset/Erc20Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Erc20TokenData } from '@/Asset/types';
import { NO_NETWORK } from '@/errors';
import { BN } from 'avalanche';
import { Contract } from 'web3-eth-contract';
import DOMPurify from 'dompurify';
import DOMPurify from 'isomorphic-dompurify';

export default class Erc20Token {
contract: Contract;
Expand Down
116 changes: 116 additions & 0 deletions src/History/api_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { ITransactionData, ITransactionDataEVM } from '@/History/raw_types';
import { explorer_api } from '@/Network/network';
import { NO_EXPLORER_API } from '@/errors';
import { ChainIdType } from '@/types';

/**
* Returns transactions FROM and TO the address given
* @param addr The address to get historic transactions for.
*/
export async function getAddressHistoryEVM(addr: string): Promise<ITransactionDataEVM[]> {
if (!explorer_api) {
throw NO_EXPLORER_API;
}

let endpoint = `v2/ctransactions?address=${addr}`;
let data: ITransactionDataEVM[] = (await explorer_api.get(endpoint)).data.Transactions;

data.sort((a, b) => {
let dateA = new Date(a.createdAt);
let dateB = new Date(b.createdAt);

return dateB.getTime() - dateA.getTime();
});

return data;
}

export async function getAddressHistory(
addrs: string[],
limit = 20,
chainID: string,
endTime?: string
): Promise<ITransactionData[]> {
if (!explorer_api) {
throw NO_EXPLORER_API;
}

const ADDR_SIZE = 1024;
let selection = addrs.slice(0, ADDR_SIZE);
let remaining = addrs.slice(ADDR_SIZE);

let addrsRaw = selection.map((addr) => {
return addr.split('-')[1];
});

let rootUrl = 'v2/transactions';

let req = {
address: addrsRaw,
sort: ['timestamp-desc'],
disableCount: ['1'],
chainID: [chainID],
disableGenesis: ['false'],
};

if (limit > 0) {
//@ts-ignore
req.limit = [limit.toString()];
}

if (endTime) {
//@ts-ignore
req.endTime = [endTime];
}

let res = await explorer_api.post(rootUrl, req);
let txs = res.data.transactions;
let next: string | undefined = res.data.next;

if (txs === null) txs = [];

// If we need to fetch more for this address
if (next && !limit) {
let endTime = next.split('&')[0].split('=')[1];
let nextRes = await getAddressHistory(selection, limit, chainID, endTime);
txs.push(...nextRes);
}

// If there are addresses left, fetch them too
// TODO: Do this in parallel, not recursive
if (remaining.length > 0) {
let nextRes = await getAddressHistory(remaining, limit, chainID);
txs.push(...nextRes);
}

return txs;
}

/**
* Returns the ortelius data from the given tx id.
* @param txID
*/
export async function getTx(txID: string): Promise<ITransactionData> {
if (!explorer_api) {
throw NO_EXPLORER_API;
}

let url = `v2/transactions/${txID}`;
let res = await explorer_api.get(url);
return res.data;
}

/**
* Returns ortelius data for a transaction hash on C chain EVM,
* @param txHash
*/
export async function getTxEvm(txHash: string): Promise<ITransactionDataEVM> {
if (!explorer_api) {
throw NO_EXPLORER_API;
}

let endpoint = `v2/ctransactions?hash=${txHash}`;
let data: ITransactionDataEVM = (await explorer_api.get(endpoint)).data.Transactions[0];

return data;
}
174 changes: 174 additions & 0 deletions src/History/base_tx_parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
iHistoryBaseTx,
iHistoryBaseTxNFTsReceived,
iHistoryBaseTxNFTsReceivedRaw,
iHistoryBaseTxNFTsSent,
iHistoryBaseTxNFTsSentRaw,
iHistoryBaseTxToken,
iHistoryBaseTxTokenLossGain,
iHistoryBaseTxTokenOwners,
ITransactionData,
UTXO,
} from '@/History';
import * as Assets from '@/Asset/Assets';
import { bnToLocaleString, getTxFeeX } from '@/utils';
import { AVMConstants } from 'avalanche/dist/apis/avm';
import { BN } from 'avalanche';
import { getAssetBalanceFromUTXOs, getNFTBalanceFromUTXOs, parseMemo } from '@/History/history_helpers';
import {
filterDuplicateStrings,
getAssetOutputs,
getNotOwnedOutputs,
getOutputsAssetIDs,
getOutputsAssetOwners,
getOutputsOfType,
getOutputTotals,
getOwnedOutputs,
} from '@/History/utxo_helpers';
import { getAvaxAssetID } from '@/Network';

export async function getBaseTxSummary(tx: ITransactionData, ownerAddrs: string[]): Promise<iHistoryBaseTx> {
let ins = tx.inputs?.map((input) => input.output) || [];
let outs = tx.outputs || [];

// Calculate losses from inputs
let losses = getOwnedTokens(ins, ownerAddrs);
let gains = getOwnedTokens(outs, ownerAddrs);

let nowOwnedIns = getNotOwnedOutputs(ins, ownerAddrs);
let nowOwnedOuts = getNotOwnedOutputs(outs, ownerAddrs);

let froms = getOutputsAssetOwners(nowOwnedIns);
let tos = getOutputsAssetOwners(nowOwnedOuts);

let tokens = await getBaseTxTokensSummary(gains, losses, froms, tos);

return {
id: tx.id,
fee: getTxFeeX(),
type: 'transaction',
timestamp: new Date(tx.timestamp),
memo: parseMemo(tx.memo),
tokens: tokens,
};
}

function getBaseTxNFTLosses(tx: ITransactionData, ownerAddrs: string[]): iHistoryBaseTxNFTsSentRaw {
let ins = tx.inputs || [];
let inUTXOs = ins.map((input) => input.output);
let nftUTXOs = inUTXOs.filter((utxo) => {
return utxo.outputType === AVMConstants.NFTXFEROUTPUTID;
});

let res: iHistoryBaseTxNFTsSentRaw = {};
for (let assetID in tx.inputTotals) {
let nftBal = getNFTBalanceFromUTXOs(nftUTXOs, ownerAddrs, assetID);

// If empty dictionary pass
if (Object.keys(nftBal).length === 0) continue;

res[assetID] = nftBal;
}
return res;
}

function getBaseTxNFTGains(tx: ITransactionData, ownerAddrs: string[]): iHistoryBaseTxNFTsReceivedRaw {
let outs = tx.outputs || [];
let nftUTXOs = outs.filter((utxo) => {
return utxo.outputType === AVMConstants.NFTXFEROUTPUTID;
});
let res: iHistoryBaseTxNFTsReceivedRaw = {};

for (let assetID in tx.inputTotals) {
let nftBal = getNFTBalanceFromUTXOs(nftUTXOs, ownerAddrs, assetID);
// If empty dictionary pass
if (Object.keys(nftBal).length === 0) continue;

res[assetID] = nftBal;
}
return res;
}

/**
* Returns a dictionary of asset totals belonging to the owner
* @param utxos
* @param ownerAddrs
*/
function getOwnedTokens(utxos: UTXO[], ownerAddrs: string[]): iHistoryBaseTxTokenLossGain {
let tokenUTXOs = getOutputsOfType(utxos, AVMConstants.SECPXFEROUTPUTID);
// Owned inputs
let myUTXOs = getOwnedOutputs(tokenUTXOs, ownerAddrs);

// Asset IDs received
let assetIDs = getOutputsAssetIDs(myUTXOs);

let res: iHistoryBaseTxTokenLossGain = {};

for (let i = 0; i < assetIDs.length; i++) {
let assetID = assetIDs[i];
let assetUTXOs = getAssetOutputs(myUTXOs, assetID);
let tot = getOutputTotals(assetUTXOs);
res[assetID] = tot;
}

return res;
}

async function getBaseTxTokensSummary(
gains: iHistoryBaseTxTokenLossGain,
losses: iHistoryBaseTxTokenLossGain,
froms: iHistoryBaseTxTokenOwners,
tos: iHistoryBaseTxTokenOwners
): Promise<iHistoryBaseTxToken[]> {
let res: iHistoryBaseTxToken[] = [];

let assetIDs = filterDuplicateStrings([...Object.keys(gains), ...Object.keys(losses)]);

// Fetch asset descriptions
let calls = assetIDs.map((id) => Assets.getAssetDescription(id));
let descs = await Promise.all(calls);
let descsDict: any = {};

// Convert array to dict
for (let i = 0; i < descs.length; i++) {
let desc = descs[i];
descsDict[desc.assetID] = desc;
}

for (let i = 0; i < assetIDs.length; i++) {
let id = assetIDs[i];
let tokenGain = gains[id] || new BN(0);
let tokenLost = losses[id] || new BN(0);
let tokenDesc = descsDict[id];

// If we sent avax, deduct the fee
if (id === getAvaxAssetID() && !tokenLost.isZero()) {
tokenLost = tokenLost.sub(getTxFeeX());
}

// How much we gained/lost of this token
let diff = tokenGain.sub(tokenLost);
let diffClean = bnToLocaleString(diff, tokenDesc.denomination);

// If we didnt gain or lose anything, ignore this token
if (diff.isZero()) continue;

if (diff.isNeg()) {
res.push({
amount: diff,
amountDisplayValue: diffClean,
addresses: tos[id],
asset: tokenDesc,
});
} else {
res.push({
amount: diff,
amountDisplayValue: diffClean,
addresses: froms[id],
asset: tokenDesc,
});
}
}

return res;
}
33 changes: 33 additions & 0 deletions src/History/evmParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ITransactionDataEVM } from '@/History/raw_types';
import { iHistoryEVMTx } from '@/History/parsed_types';
import { bnToAvaxC } from '@/utils';
import { BN } from 'avalanche';

export function getTransactionSummaryEVM(tx: ITransactionDataEVM, walletAddress: string): iHistoryEVMTx {
let isSender = tx.fromAddr.toUpperCase() === walletAddress.toUpperCase();

let amt = new BN(tx.value);
let amtClean = bnToAvaxC(amt);
let date = new Date(tx.createdAt);

let gasLimit = new BN(tx.gasLimit);
let gasPrice = new BN(tx.gasPrice);
let feeBN = gasLimit.mul(gasPrice); // in gwei

return {
id: tx.hash,
fee: feeBN,
memo: '',
block: tx.block,
isSender,
type: 'transaction_evm',
amount: amt,
amountDisplayValue: amtClean,
gasLimit: tx.gasLimit,
gasPrice: tx.gasPrice,
from: tx.fromAddr,
to: tx.toAddr,
timestamp: date,
input: tx.input,
};
}
Loading

0 comments on commit d148f24

Please sign in to comment.