Skip to content

Commit

Permalink
Merge branch 'box-consolidations' into 'dev'
Browse files Browse the repository at this point in the history
Box consolidations

See merge request ergo/minotaur/minotaur-wallet!31
  • Loading branch information
vorujack committed Jun 29, 2024
2 parents b15b686 + 9e863fe commit 69c451b
Show file tree
Hide file tree
Showing 23 changed files with 656 additions and 247 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@minotaur-ergo/icons": "^0.1.4",
"@minotaur-ergo/icons": "^0.1.5",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@reduxjs/toolkit": "^2.2.5",
Expand Down
4 changes: 2 additions & 2 deletions src/action/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ const verifyAddress = async (addressId: number) => {
console.warn(
`address balance incompatible with address boxes. reset address. addressId: [${addressId}]`,
);
BoxDbAction.getInstance().deleteBoxForAddress(addressId);
AddressDbAction.getInstance().updateAddressHeight(addressId, 0);
await BoxDbAction.getInstance().deleteBoxForAddress(addressId);
await AddressDbAction.getInstance().updateAddressHeight(addressId, 0);
}
};

Expand Down
6 changes: 6 additions & 0 deletions src/db/entities/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class Wallet {

@Column('int', { nullable: false, default: 1 })
version = 1;

@Column('text', { default: '' })
flags = '';

@Column('text', { default: '' })
encrypted_mnemonic = '';
}

export default Wallet;
Expand Down
28 changes: 28 additions & 0 deletions src/db/migration/1719388610915-spend-box-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class SpendBoxInfo1719388610915 implements MigrationInterface {
name = 'SpendBoxInfo1719388610915';
public async up(queryRunner: QueryRunner): Promise<void> {
const queries = [
'CREATE TABLE "temporary_wallet" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text NOT NULL, "network_type" text NOT NULL, "seed" text NOT NULL, "extended_public_key" text NOT NULL, "type" text NOT NULL, "required_sign" integer, "version" integer NOT NULL DEFAULT (1), "flags" text NOT NULL DEFAULT (\'\'), "encrypted_mnemonic" text NOT NULL DEFAULT (\'\'))',
'INSERT INTO "temporary_wallet"("id", "name", "network_type", "seed", "extended_public_key", "type", "required_sign", "version") SELECT "id", "name", "network_type", "seed", "extended_public_key", "type", "required_sign", "version" FROM "wallet"',
'DROP TABLE "wallet"',
'ALTER TABLE "temporary_wallet" RENAME TO "wallet"',
];
for (const query of queries) {
await queryRunner.query(query);
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
const queries = [
'CREATE TABLE "temporary_wallet" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text NOT NULL, "network_type" text NOT NULL, "seed" text NOT NULL, "extended_public_key" text NOT NULL, "type" text NOT NULL, "required_sign" integer, "version" integer NOT NULL DEFAULT (1))',
'INSERT INTO "temporary_wallet"("id", "name", "network_type", "seed", "extended_public_key", "type", "required_sign", "version") SELECT "id", "name", "network_type", "seed", "extended_public_key", "type", "required_sign", "version" FROM "wallet"',
'DROP TABLE "wallet"',
'ALTER TABLE "temporary_wallet" RENAME TO "wallet"',
];
for (const query of queries) {
await queryRunner.query(query);
}
}
}
3 changes: 2 additions & 1 deletion src/db/migration/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SpendBoxInfo1719388610915 } from './1719388610915-spend-box-info';
import { initialize1687534587363 } from './1687534587363-initialize';

const Migrations = [initialize1687534587363];
const Migrations = [initialize1687534587363, SpendBoxInfo1719388610915];

export default Migrations;
1 change: 1 addition & 0 deletions src/hooks/useDAppConnectorProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const useDAppConnectorProps = (wallet: StateWallet): DAppPropsType => {
const message = useContext(MessageContext);
const txSign = useContext(TxSignContext);
return {
walletId: wallet.id,
getAddresses: async () => wallet.addresses.map((item) => item.address),
getCoveringForErgAndToken: selectBoxesDApps(wallet),
chain: getChain(wallet.networkType),
Expand Down
57 changes: 57 additions & 0 deletions src/hooks/useWalletTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
getWalletTransactions,
getWalletTransactionsTotal,
WalletTransactionType,
} from '@/action/transaction';
import { StateWallet } from '@/store/reducer/wallet';
import { useEffect, useState } from 'react';

const useWalletTransaction = (wallet: StateWallet, limit: number) => {
const [transactions, setTransactions] = useState<
Array<WalletTransactionType>
>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [resetTransactions, setResetTransactions] = useState(false);
const [heights, setHeights] = useState<string>('');
useEffect(() => {
const currentHeights = JSON.stringify(
wallet.addresses.map((item) => item.proceedHeight),
);
if (!loading && currentHeights !== heights) {
setLoading(true);
getWalletTransactionsTotal(wallet.id).then((newTotal) => {
if (newTotal !== total) {
setTotal(newTotal);
setResetTransactions(true);
}
setHeights(currentHeights);
setLoading(false);
});
}
}, [wallet.addresses, wallet.id, loading, heights, total]);
useEffect(() => {
if (
!loading &&
(transactions.length < Math.min(total, limit) || resetTransactions)
) {
setLoading(true);
getWalletTransactions(
wallet.id,
transactions.length,
limit - transactions.length,
).then((newTransactions) => {
setTransactions([...transactions, ...newTransactions]);
setResetTransactions(false);
setLoading(false);
});
}
}, [loading, transactions, total, limit, wallet.id, resetTransactions]);
return {
transactions,
loading,
total,
};
};

export default useWalletTransaction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
Button,
Divider,
FormControl,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
Typography,
} from '@mui/material';
import { DAppPropsType, UnsignedGeneratedTx } from '@/types/dapps';
import React, { useState } from 'react';
import DisplayId from '@/components/display-id/DisplayId';
import useAddresses from './useAddresses';
import useAddressBoxes from './useAddressBoxes';
import UnspentBoxesCount from './UnspentBoxesCount';
import OldestBoxAge from './OldestBoxAge';
import getColor from './getColor';
import * as wasm from 'ergo-lib-wasm-browser';
import { CONSOLIDATE_FEE, IMPLEMENTOR, TRANSACTION_FEE } from './parameters';
import { boxesToArrayBox } from '@/utils/convert';
import LoadingPage from '@/components/loading-page/LoadingPage';

const BoxConsolidation = (props: DAppPropsType) => {
const addresses = useAddresses(props);
const [selectedAddress, setSelectedAddress] = useState(0);
const state = useAddressBoxes(addresses.addresses, selectedAddress, props);
const [generating, setGenerating] = useState(false);
const handleChangeAddress = (event: SelectChangeEvent) => {
const index = parseInt(event.target.value);
if (!isNaN(index) && index >= 0 && index < addresses.addresses.length) {
setSelectedAddress(index);
}
};
const boxCountConsolidate = state.boxesCount >= 100;
const ageConsolidate = state.oldestAge >= 3.0;
const consolidate = boxCountConsolidate || ageConsolidate;
const generateTransaction = () => {
if (!generating) {
setGenerating(true);
props
.getCoveringForErgAndToken(
999999999000000000n,
[],
addresses.addresses[selectedAddress],
)
.then((covering) => {
const height = state.height;
const boxArray = boxesToArrayBox(covering.boxes);
const totalErg = boxArray
.map((item) => BigInt(item.value().as_i64().to_str()))
.reduce((a, b) => a + b, 0n);
const outputCandidates = wasm.ErgoBoxCandidates.empty();
const consolidated = new wasm.ErgoBoxCandidateBuilder(
wasm.BoxValue.from_i64(
wasm.I64.from_str(
(totalErg - TRANSACTION_FEE - CONSOLIDATE_FEE).toString(),
),
),
wasm.Contract.pay_to_address(
wasm.Address.from_base58(addresses.addresses[selectedAddress]),
),
height,
);
const tokensAmount = new Map<string, bigint>();
boxArray.forEach((box) => {
const tokens = box.tokens();
for (let index = 0; index < tokens.len(); index++) {
const token = tokens.get(index);
const tokenId = token.id().to_str();
const amount = BigInt(token.amount().as_i64().to_str());
tokensAmount.set(
tokenId,
amount + (tokensAmount.get(tokenId) ?? 0n),
);
}
});
tokensAmount.forEach((value, key) => {
consolidated.add_token(
wasm.TokenId.from_str(key),
wasm.TokenAmount.from_i64(wasm.I64.from_str(value.toString())),
);
});
outputCandidates.add(consolidated.build());
if (CONSOLIDATE_FEE > 0n) {
const feeBox = new wasm.ErgoBoxCandidateBuilder(
wasm.BoxValue.from_i64(
wasm.I64.from_str(CONSOLIDATE_FEE.toString()),
),
wasm.Contract.pay_to_address(
wasm.Address.from_base58(IMPLEMENTOR),
),
height,
).build();
outputCandidates.add(feeBox);
}
const tx = wasm.TxBuilder.new(
new wasm.BoxSelection(
covering.boxes,
new wasm.ErgoBoxAssetsDataList(),
),
outputCandidates,
height,
wasm.BoxValue.from_i64(
wasm.I64.from_str(TRANSACTION_FEE.toString()),
),
wasm.Address.from_base58(addresses.addresses[selectedAddress]),
).build();
const reduced = wasm.ReducedTransaction.from_unsigned_tx(
tx,
covering.boxes,
wasm.ErgoBoxes.empty(),
props.chain.fakeContext(),
);
const request: UnsignedGeneratedTx = {
boxes: covering.boxes,
tx: reduced,
};
props.signAndSendTx(request);
setGenerating(false);
});
}
};
return (
<React.Fragment>
<FormControl sx={{ mb: 2 }}>
<InputLabel>Address</InputLabel>
<Select
value={selectedAddress.toString()}
onChange={handleChangeAddress}
>
{addresses.addresses.map((item, index) => (
<MenuItem key={item} value={index.toString()}>
<DisplayId id={item} />
</MenuItem>
))}
</Select>
</FormControl>
{addresses.loading || state.loading ? (
<LoadingPage title="Loading address boxes" description="Please wait" />
) : state.boxesCount === 0 ? (
<Typography>There is no boxes in this address</Typography>
) : (
<React.Fragment>
<UnspentBoxesCount
consolidate={boxCountConsolidate}
boxCount={state.boxesCount}
/>
<OldestBoxAge age={state.oldestAge} consolidate={ageConsolidate} />
<Divider />

<Typography
textAlign="center"
fontSize="x-large"
color={getColor(consolidate)}
mt={3}
mb={2}
>
{consolidate ? 'Consider consolidation' : 'You are fine!'}
</Typography>
<Typography
textAlign="center"
variant="body2"
color="textSecondary"
mb={3}
>
You can renew {state.boxesCount} box
{state.boxesCount > 1 ? 'es' : ''}{' '}
{consolidate ? '' : ', but it is not necessary.'}
</Typography>
<Button onClick={generateTransaction}>
{/* {generating ? <Loading/> : undefined} */}
Renew {state.boxesCount} box{state.boxesCount > 1 ? 'es' : ''}
</Button>
</React.Fragment>
)}
</React.Fragment>
);
};

export default BoxConsolidation;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Typography } from '@mui/material';
import React from 'react';
import getColor from './getColor';
import { CheckCircleOutline, WarningAmberRounded } from '@mui/icons-material';

interface ConsolidateIndicatorPropsType {
consolidate: boolean;
}
const ConsolidateIndicator = (props: ConsolidateIndicatorPropsType) => {
return (
<React.Fragment>
<Typography
color={getColor(props.consolidate)}
fontWeight={500}
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
>
{props.consolidate ? <WarningAmberRounded /> : <CheckCircleOutline />}
{props.consolidate ? 'Consider consolidation' : "It's fine"}
</Typography>
</React.Fragment>
);
};

export default ConsolidateIndicator;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Typography } from '@mui/material';
import React from 'react';
import ConsolidateIndicator from './ConsolidateIndicator';

interface OldestBoxAgePropsType {
age: number;
consolidate: boolean;
}

const OldestBoxAge = (props: OldestBoxAgePropsType) => {
return (
<React.Fragment>
<Typography variant="body2" color="textSecondary">
Age of oldest box
</Typography>
<Typography mb={1}>
<Typography component="span" fontSize="large">
{props.age.toFixed(2)}
</Typography>{' '}
years
</Typography>
<ConsolidateIndicator consolidate={props.consolidate} />
<Typography variant="body2" color="textSecondary" pl={4} mb={3}>
Storage rent will be applied on boxes older than 4 years.
</Typography>
</React.Fragment>
);
};

export default OldestBoxAge;
Loading

0 comments on commit 69c451b

Please sign in to comment.