Skip to content

Commit

Permalink
feat: add nano contract transaction components
Browse files Browse the repository at this point in the history
  • Loading branch information
alexruzenhack committed Apr 1, 2024
1 parent ee0be8a commit a0caced
Show file tree
Hide file tree
Showing 4 changed files with 494 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import React, { useEffect, useState } from 'react';
import { StyleSheet, View, FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import { transactionUtils } from '@hathor/wallet-lib';
import { t } from 'ttag';

import { COLORS } from '../../styles/themes';
import { NanoContractTransactionBalanceListItem } from './NanoContractTransactionBalanceListItem.component';
import { HathorFlatList } from '../HathorFlatList.component';

/**
* Calculate the balance of a transaction
*
* @param tx Transaction to get balance from
* @param storage Storage to get metadata from
* @returns {Promise<Record<string, IBalance>>} Balance of the transaction
*/
async function getTxBalance(tx, storage) {
const balance = {};
const getEmptyBalance = () => ({
tokens: { locked: 0, unlocked: 0 },
authorities: {
mint: { locked: 0, unlocked: 0 },
melt: { locked: 0, unlocked: 0 },
},
});

const nowTs = Math.floor(Date.now() / 1000);
const nowHeight = await storage.getCurrentHeight();
const rewardLock = storage.version?.reward_spend_min_blocks;
const isHeightLocked = this.isHeightLocked(tx.height, nowHeight, rewardLock);

for (const output of tx.outputs) {
// Removed isAddressMine filter
if (!balance[output.token]) {
balance[output.token] = getEmptyBalance();
}
const isLocked = this.isOutputLocked(output, { refTs: nowTs }) || isHeightLocked;

if (this.isAuthorityOutput(output)) {
if (this.isMint(output)) {
if (isLocked) {
balance[output.token].authorities.mint.locked += 1;
} else {
balance[output.token].authorities.mint.unlocked += 1;
}
}
if (this.isMelt(output)) {
if (isLocked) {
balance[output.token].authorities.melt.locked += 1;
} else {
balance[output.token].authorities.melt.unlocked += 1;
}
}
} else if (isLocked) {
balance[output.token].tokens.locked += output.value;
} else {
balance[output.token].tokens.unlocked += output.value;
}
}

for (const input of tx.inputs) {
// Removed isAddressMine filter
if (!balance[input.token]) {
balance[input.token] = getEmptyBalance();
}

if (this.isAuthorityOutput(input)) {
if (this.isMint(input)) {
balance[input.token].authorities.mint.unlocked -= 1;
}
if (this.isMelt(input)) {
balance[input.token].authorities.melt.unlocked -= 1;
}
} else {
balance[input.token].tokens.unlocked -= input.value;
}
}

return balance;
}

async function getTokensBalance(tx, wallet) {
const tokensBalance = await getTxBalance.bind(transactionUtils)(tx, wallet.storage);
const balances = [];
for (const [key, balance] of Object.entries(tokensBalance)) {
const tokenBalance = {
tokenUid: key,
available: balance.tokens.unlocked,
locked: balance.tokens.locked
};
balances.push(tokenBalance);
}
return balances;
}

export const NanoContractTransactionBalanceList = ({ tx }) => {
const wallet = useSelector((state) => state.wallet);
const [tokensBalance, setTokensBalance] = useState([]);

useEffect(() => {
const fetchTokensBalance = async () => {
const balance = await getTokensBalance(tx, wallet);
console.log('tokens balance', balance);
setTokensBalance(balance);
};
fetchTokensBalance();
}, []);

return (
<Wrapper>
<HathorFlatList
data={tokensBalance}
renderItem={({ item, index }) => (
<NanoContractTransactionBalanceListItem
item={item}
index={index}
/>
)}
keyExtractor={(item) => item.tokenUid}
/>
</Wrapper>
);
};

const Wrapper = ({ children }) => (
<View style={styles.wrapper}>
{children}
</View>
);

const styles = StyleSheet.create({
wrapper: {
flex: 1,
alignSelf: 'stretch',
backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import {
TouchableHighlight,
StyleSheet,
View,
Text,
} from 'react-native';
import { useSelector } from 'react-redux';
import { t } from 'ttag';

import { COLORS } from '../../styles/themes';
import { getShortHash, isTokenNFT, renderValue } from '../../utils';
import { ReceivedIcon } from '../Icon/Received.icon';
import { SentIcon } from '../Icon/Sent.icon';

function getBalanceType(value) {
if (value < 0) {
return 'sent';
}
return 'received';
}

function getTokenValue(tokenUid, tokens) {
const registeredToken = Object.values(tokens).filter((token) => token.uid === tokenUid).pop();
if (registeredToken) {
return registeredToken.symbol;
}
return getShortHash(tokenUid, 7);
}

/**
* Renders each item of Nano Contract Transactions List.
*
* @param {Object} ncItem
* @property {Object} ncItem.item registered Nano Contract data
* @property {number} ncItem.index position in the list
*/
export const NanoContractTransactionBalanceListItem = ({ item, index }) => {
const balance = item.available + item.locked;
const tokens = useSelector((state) => state.tokens) || {};
const tokenValue = getTokenValue(item.tokenUid, tokens);
const type = getBalanceType(balance);
const tokensMetadata = useSelector((state) => state.tokenMetadata);
const isNft = isTokenNFT(item.tokenUid, tokensMetadata);

return (
<Wrapper index={index}>
<Icon type={type} />
<ContentWrapper tokenValue={tokenValue} type={type} />
<BalanceValue balance={balance} isNft={isNft} />
</Wrapper>
);
};

const Wrapper = ({ index, children }) => {
const isFirstItem = index === 0;
return (
<TouchableHighlight
style={[isFirstItem && styles.firstItem]}
underlayColor={COLORS.primaryOpacity30}
>
<View style={styles.wrapper}>{children}</View>
</TouchableHighlight>
);
};

const Icon = ({ type }) => {
const iconMap = {
sent: SentIcon({ type: 'default' }),
received: ReceivedIcon({ type: 'default' }),
};

return (iconMap[type]);
};

/**
* Renders item core content.
*
* @param {Object} ncItem
* @property {Obeject} ncItem.nc registered Nano Contract data
*/
const ContentWrapper = ({ tokenValue, type }) => {
const contentMap = {
sent: t`Sent ${tokenValue}`,
received: t`Received ${tokenValue}`,
};

return (
<View style={styles.contentWrapper}>
<Text style={[styles.text, styles.property]}>{contentMap[type]}</Text>
</View>
);
};

const BalanceValue = ({ balance, isNft }) => {
const isReceivedType = getBalanceType(balance) === 'received';
const balanceValue = renderValue(balance, isNft);

return (
<View style={styles.balanceWrapper}>
<Text style={[
styles.balance,
isReceivedType && styles.balanceReceived,
]}
>
{balanceValue}
</Text>
</View>
)
};

const styles = StyleSheet.create({
wrapper: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
width: '100%',
paddingVertical: 24,
paddingHorizontal: 16,
},
firstItem: {
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
},
contentWrapper: {
maxWidth: '80%',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'space-between',
marginRight: 'auto',
paddingHorizontal: 16,
},
icon: {
alignSelf: 'flex-start',
},
text: {
fontSize: 14,
lineHeight: 20,
paddingBottom: 6,
color: 'hsla(0, 0%, 38%, 1)',
},
property: {
paddingBottom: 4,
fontWeight: 'bold',
color: 'black',
},
padding0: {
paddingBottom: 0,
},
balanceWrapper: {
marginLeft: 'auto',
},
balance: {
fontSize: 16,
lineHeight: 20,
},
balanceReceived: {
color: 'hsla(180, 85%, 34%, 1)',
fontWeight: 'bold',
},
});
Loading

0 comments on commit a0caced

Please sign in to comment.