only show and fetch own transactions (#107)

* fixed button with no provider

* feat: fix bridge transactions

* fix: connection selection

* only show and fetch own transactions

* Added better messages

* added memo to columns

Co-authored-by: bartosz-lipinski <264380+bartosz-lipinski@users.noreply.github.com>
This commit is contained in:
Juan Diego García 2021-05-01 19:59:01 -05:00 committed by GitHub
parent ea2af3b06f
commit 18bda5527f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 352 additions and 257 deletions

View File

@ -1,4 +1,4 @@
import { programIds, sendTransactionWithRetry } from '@oyster/common'; import { programIds, sendTransactionWithRetry, sleep } from '@oyster/common';
import { WalletAdapter } from '@solana/wallet-base'; import { WalletAdapter } from '@solana/wallet-base';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { WormholeFactory } from '../../contracts/WormholeFactory'; import { WormholeFactory } from '../../contracts/WormholeFactory';
@ -113,6 +113,17 @@ export const fromSolana = async (
wallet, wallet,
[ix, fee_ix, lock_ix], [ix, fee_ix, lock_ix],
[], [],
undefined,
false,
undefined,
() => {
setProgress({
message: 'Executing Solana Transaction',
type: 'wait',
group,
step: counter++,
});
},
); );
return steps.wait(request, transferKey, slot); return steps.wait(request, transferKey, slot);
@ -124,32 +135,40 @@ export const fromSolana = async (
) => { ) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
let completed = false; let completed = false;
let unsubscribed = false;
let startSlot = slot; let startSlot = slot;
let group = 'Lock assets'; let group = 'Lock assets';
const solConfirmationMessage = (current: number) => const solConfirmationMessage = (current: number) =>
`Awaiting ETH confirmations: ${current} out of 32`; `Awaiting Solana confirmations: ${current} out of 32`;
let replaceMessage = false;
let slotUpdateListener = connection.onSlotChange(slot => { let slotUpdateListener = connection.onSlotChange(slot => {
if (completed) return; if (unsubscribed) {
const passedSlots = slot.slot - startSlot; return;
}
const passedSlots = Math.min(Math.max(slot.slot - startSlot, 0), 32);
const isLast = passedSlots - 1 === 31; const isLast = passedSlots - 1 === 31;
if (passedSlots < 32) { if (passedSlots <= 32) {
setProgress({ setProgress({
message: solConfirmationMessage(passedSlots), message: solConfirmationMessage(passedSlots),
type: isLast ? 'done' : 'wait', type: isLast ? 'done' : 'wait',
step: counter++, step: counter++,
group, group,
replace: passedSlots > 0, replace: replaceMessage,
}); });
if (isLast) { replaceMessage = true;
}
if (completed || isLast) {
unsubscribed = true;
setProgress({ setProgress({
message: 'Awaiting guardian confirmation', message: 'Awaiting guardian confirmation. (Up to few min.)',
type: 'wait', type: 'wait',
step: counter++, step: counter++,
group, group,
}); });
} }
}
}); });
let accountChangeListener = connection.onAccountChange( let accountChangeListener = connection.onAccountChange(
@ -175,10 +194,19 @@ export const fromSolana = async (
completed = true; completed = true;
connection.removeAccountChangeListener(accountChangeListener); connection.removeAccountChangeListener(accountChangeListener);
connection.removeSlotChangeListener(slotUpdateListener); connection.removeSlotChangeListener(slotUpdateListener);
let signatures;
let signatures = await bridge.fetchSignatureStatus( while (!signatures) {
try {
signatures = await bridge.fetchSignatureStatus(
lockup.signatureAccount, lockup.signatureAccount,
); );
break;
} catch {
await sleep(500);
}
}
let sigData = Buffer.of( let sigData = Buffer.of(
...signatures.reduce((previousValue, currentValue) => { ...signatures.reduce((previousValue, currentValue) => {
previousValue.push(currentValue.index); previousValue.push(currentValue.index);
@ -217,7 +245,7 @@ export const fromSolana = async (
}); });
let tx = await wh.submitVAA(vaa); let tx = await wh.submitVAA(vaa);
setProgress({ setProgress({
message: 'Waiting for tokens unlock to be mined...', message: 'Waiting for tokens unlock to be mined... (Up to few min.)',
type: 'wait', type: 'wait',
group, group,
step: counter++, step: counter++,

View File

@ -1,5 +1,5 @@
import { BigNumber } from 'bignumber.js'; import { BigNumber } from 'bignumber.js';
import {ethers} from "ethers"; import { ethers } from 'ethers';
import { ASSET_CHAIN } from '../constants'; import { ASSET_CHAIN } from '../constants';
export interface ProgressUpdate { export interface ProgressUpdate {

View File

@ -21,7 +21,7 @@ import {
import { AccountInfo } from '@solana/spl-token'; import { AccountInfo } from '@solana/spl-token';
import { TransferRequest, ProgressUpdate } from './interface'; import { TransferRequest, ProgressUpdate } from './interface';
import { WalletAdapter } from '@solana/wallet-base'; import { WalletAdapter } from '@solana/wallet-base';
import { BigNumber } from "bignumber.js"; import { BigNumber } from 'bignumber.js';
export const toSolana = async ( export const toSolana = async (
connection: Connection, connection: Connection,
@ -180,7 +180,7 @@ export const toSolana = async (
}); });
let res = await e.approve(programIds().wormhole.bridge, amountBN); let res = await e.approve(programIds().wormhole.bridge, amountBN);
setProgress({ setProgress({
message: 'Waiting for ETH transaction to be mined...', message: 'Waiting for ETH transaction to be mined... (Up to few min.)',
type: 'wait', type: 'wait',
group, group,
step: counter++, step: counter++,
@ -243,7 +243,7 @@ export const toSolana = async (
false, false,
); );
setProgress({ setProgress({
message: 'Waiting for ETH transaction to be mined...', message: 'Waiting for ETH transaction to be mined... (Up to few min.)',
type: 'wait', type: 'wait',
group, group,
step: counter++, step: counter++,

View File

@ -7,6 +7,16 @@ import * as BufferLayout from 'buffer-layout';
import * as bs58 from 'bs58'; import * as bs58 from 'bs58';
import { AssetMeta } from '../bridge'; import { AssetMeta } from '../bridge';
export enum LockupStatus {
AWAITING_VAA,
UNCLAIMED_VAA,
COMPLETED,
}
export interface LockupWithStatus extends Lockup {
status: LockupStatus;
}
export interface Lockup { export interface Lockup {
lockupAddress: PublicKey; lockupAddress: PublicKey;
amount: BN; amount: BN;

View File

@ -1,5 +1,5 @@
import { Button, Table, Tabs, notification } from 'antd'; import { Button, Table, Tabs, notification } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import './index.less'; import './index.less';
@ -34,11 +34,7 @@ const { TabPane } = Tabs;
export const RecentTransactionsTable = (props: { export const RecentTransactionsTable = (props: {
showUserTransactions?: boolean; showUserTransactions?: boolean;
}) => { }) => {
const { const { loading: loadingTransfers, transfers } = useWormholeTransactions();
loading: loadingTransfers,
transfers,
userTransfers,
} = useWormholeTransactions();
const { provider } = useEthereum(); const { provider } = useEthereum();
const bridge = useBridge(); const bridge = useBridge();
@ -154,26 +150,9 @@ export const RecentTransactionsTable = (props: {
}, },
}, },
]; ];
const columns = [
...baseColumns,
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render(text: string, record: any) {
return {
props: { style: {} },
children: (
<span className={`${record.status?.toLowerCase()}`}>
{record.status}
</span>
),
};
},
},
];
const userColumns = [ const userColumns = useMemo(
() => [
...baseColumns, ...baseColumns,
{ {
title: 'Status', title: 'Status',
@ -228,7 +207,9 @@ export const RecentTransactionsTable = (props: {
...signatures.reduce( ...signatures.reduce(
(previousValue, currentValue) => { (previousValue, currentValue) => {
previousValue.push(currentValue.index); previousValue.push(currentValue.index);
previousValue.push(...currentValue.signature); previousValue.push(
...currentValue.signature,
);
return previousValue; return previousValue;
}, },
@ -261,7 +242,7 @@ export const RecentTransactionsTable = (props: {
...activeSteps, ...activeSteps,
{ {
message: message:
'Waiting for tokens unlock to be mined...', 'Waiting for tokens unlock to be mined... (Up to few min.)',
type: 'wait', type: 'wait',
group, group,
step: counter++, step: counter++,
@ -277,6 +258,10 @@ export const RecentTransactionsTable = (props: {
step: counter++, step: counter++,
}, },
]); ]);
setCompletedVAAs([
...completedVAAs,
record.txhash,
]);
} }
})(); })();
}, [setActiveSteps]); }, [setActiveSteps]);
@ -339,36 +324,23 @@ export const RecentTransactionsTable = (props: {
}; };
}, },
}, },
]; ],
[completedVAAs, bridge, provider],
);
return ( return (
<div id={'recent-tx-container'}> <div id={'recent-tx-container'}>
<div className={'home-subtitle'} style={{ marginBottom: '70px' }}> <div className={'home-subtitle'} style={{ marginBottom: '70px' }}>
Transactions My Recent Transactions
</div> </div>
<Tabs defaultActiveKey="1" centered>
<TabPane tab="Recent Transactions" key="1">
<Table <Table
scroll={{ scroll={{
scrollToFirstRowOnChange: false, scrollToFirstRowOnChange: false,
x: 900, x: 900,
}} }}
dataSource={transfers.sort((a, b) => b.date - a.date)} dataSource={transfers.sort((a, b) => b.date - a.date)}
columns={columns}
loading={loadingTransfers}
/>
</TabPane>
<TabPane tab="My Transactions" key="2">
<Table
scroll={{
scrollToFirstRowOnChange: false,
x: 900,
}}
dataSource={userTransfers.sort((a, b) => b.date - a.date)}
columns={userColumns} columns={userColumns}
loading={loadingTransfers} loading={loadingTransfers}
/> />
</TabPane>
</Tabs>
</div> </div>
); );
}; };

View File

@ -14,7 +14,7 @@ export const useCorrectNetwork = () => {
if (chainId === 5) { if (chainId === 5) {
setHasCorrespondingNetworks(env === 'testnet'); setHasCorrespondingNetworks(env === 'testnet');
} else if (chainId === 1) { } else if (chainId === 1) {
setHasCorrespondingNetworks(env === 'mainnet-beta'); setHasCorrespondingNetworks(env.includes('mainnet-beta'));
} else { } else {
setHasCorrespondingNetworks(false); setHasCorrespondingNetworks(false);
} }

View File

@ -1,27 +1,33 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { import {
notify,
programIds,
useConnection, useConnection,
useConnectionConfig, useConnectionConfig,
programIds,
notify,
useWallet, useWallet,
ParsedAccountBase,
} from '@oyster/common'; } from '@oyster/common';
import { import {
WORMHOLE_PROGRAM_ID,
POSTVAA_INSTRUCTION, POSTVAA_INSTRUCTION,
TRANSFER_ASSETS_OUT_INSTRUCTION, TRANSFER_ASSETS_OUT_INSTRUCTION,
WORMHOLE_PROGRAM_ID,
} from '../utils/ids'; } from '../utils/ids';
import { ASSET_CHAIN } from '../utils/assets'; import { ASSET_CHAIN } from '../utils/assets';
import { useEthereum } from '../contexts'; import { useEthereum } from '../contexts';
import { import {
AccountInfo,
Connection, Connection,
ParsedAccountData,
PartiallyDecodedInstruction, PartiallyDecodedInstruction,
PublicKey, PublicKey,
RpcResponseAndContext,
} from '@solana/web3.js'; } from '@solana/web3.js';
import { import {
bridgeAuthorityKey, bridgeAuthorityKey,
LockupStatus,
LockupWithStatus,
SolanaBridge,
TransferOutProposalLayout, TransferOutProposalLayout,
WormholeFactory,
} from '@solana/bridge-sdk'; } from '@solana/bridge-sdk';
import bs58 from 'bs58'; import bs58 from 'bs58';
@ -31,11 +37,10 @@ import {
useCoingecko, useCoingecko,
} from '../contexts/coingecko'; } from '../contexts/coingecko';
import { BigNumber } from 'bignumber.js'; import { BigNumber } from 'bignumber.js';
import { WormholeFactory } from '@solana/bridge-sdk';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { useBridge } from '../contexts/bridge'; import { useBridge } from '../contexts/bridge';
import { SolanaBridge } from '@solana/bridge-sdk';
import BN from 'bn.js'; import BN from 'bn.js';
import { keccak256 } from 'ethers/utils';
type WrappedTransferMeta = { type WrappedTransferMeta = {
chain: number; chain: number;
@ -59,6 +64,86 @@ type WrappedTransferMeta = {
const transferCache = new Map<string, WrappedTransferMeta>(); const transferCache = new Map<string, WrappedTransferMeta>();
const queryOwnWrappedMetaTransactions = async (
authorityKey: PublicKey,
connection: Connection,
setTransfers: (arr: WrappedTransferMeta[]) => void,
provider: ethers.providers.Web3Provider,
bridge?: SolanaBridge,
owner?: PublicKey | null,
) => {
if (owner && bridge) {
const transfers = new Map<string, WrappedTransferMeta>();
let wh = WormholeFactory.connect(programIds().wormhole.bridge, provider);
const res: RpcResponseAndContext<
Array<{ pubkey: PublicKey; account: AccountInfo<ParsedAccountData> }>
> = await connection.getParsedTokenAccountsByOwner(
owner,
{ programId: programIds().token },
'single',
);
let lockups: LockupWithStatus[] = [];
for (const acc of res.value) {
const accLockups = await bridge.fetchTransferProposals(acc.pubkey);
lockups.push(
...accLockups.map(v => {
return {
status: LockupStatus.AWAITING_VAA,
...v,
};
}),
);
for (let lockup of lockups) {
if (lockup.vaaTime === undefined || lockup.vaaTime === 0) continue;
let signingData = lockup.vaa.slice(lockup.vaa[5] * 66 + 6);
for (let i = signingData.length; i > 0; i--) {
if (signingData[i] == 0xff) {
signingData = signingData.slice(0, i);
break;
}
}
let hash = keccak256(signingData);
let submissionStatus = await wh.consumedVAAs(hash);
lockup.status = submissionStatus
? LockupStatus.COMPLETED
: LockupStatus.UNCLAIMED_VAA;
}
}
for (const ls of lockups) {
const txhash = ls.lockupAddress.toBase58();
let assetAddress: string = '';
if (ls.assetChain !== ASSET_CHAIN.Solana) {
assetAddress = Buffer.from(ls.assetAddress.slice(12)).toString('hex');
} else {
assetAddress = new PublicKey(ls.assetAddress).toBase58();
}
const dec = new BigNumber(10).pow(new BigNumber(ls.assetDecimals));
const rawAmount = new BigNumber(ls.amount.toString());
const amount = rawAmount.div(dec).toNumber();
transfers.set(txhash, {
publicKey: ls.lockupAddress,
amount,
date: ls.vaaTime,
chain: ls.assetChain,
address: assetAddress,
decimals: 9,
txhash,
explorer: `https://explorer.solana.com/address/${txhash}`,
lockup: ls,
status:
ls.status === LockupStatus.UNCLAIMED_VAA
? 'Failed'
: ls.status === LockupStatus.AWAITING_VAA
? 'In Process'
: 'Completed',
});
}
setTransfers([...transfers.values()]);
}
};
const queryWrappedMetaTransactions = async ( const queryWrappedMetaTransactions = async (
authorityKey: PublicKey, authorityKey: PublicKey,
connection: Connection, connection: Connection,
@ -238,12 +323,11 @@ export const useWormholeTransactions = () => {
const { tokenMap: ethTokens } = useEthereum(); const { tokenMap: ethTokens } = useEthereum();
const { tokenMap } = useConnectionConfig(); const { tokenMap } = useConnectionConfig();
const { coinList } = useCoingecko(); const { coinList } = useCoingecko();
const { wallet, connected: walletConnected } = useWallet(); const { wallet } = useWallet();
const bridge = useBridge(); const bridge = useBridge();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [transfers, setTransfers] = useState<WrappedTransferMeta[]>([]); const [transfers, setTransfers] = useState<WrappedTransferMeta[]>([]);
const [userTransfers, setUserTransfers] = useState<WrappedTransferMeta[]>([]);
const [amountInUSD, setAmountInUSD] = useState<number>(0); const [amountInUSD, setAmountInUSD] = useState<number>(0);
useEffect(() => { useEffect(() => {
@ -263,26 +347,17 @@ export const useWormholeTransactions = () => {
(window as any).ethereum, (window as any).ethereum,
); );
// query wrapped assets that were imported to solana from other chains // query wrapped assets that were imported to solana from other chains
queryWrappedMetaTransactions( queryOwnWrappedMetaTransactions(
authorityKey, authorityKey,
connection, connection,
setTransfers, setTransfers,
provider, provider,
bridge, bridge,
wallet?.publicKey,
).then(() => setLoading(false)); ).then(() => setLoading(false));
} }
})(); })();
}, [connection, setTransfers]); }, [connection, setTransfers, wallet?.publicKey]);
useEffect(() => {
if (transfers && walletConnected && wallet?.publicKey) {
setUserTransfers(
transfers.filter(t => {
return t.owner === wallet?.publicKey?.toBase58();
}),
);
}
}, [wallet, walletConnected, transfers]);
const coingeckoTimer = useRef<number>(0); const coingeckoTimer = useRef<number>(0);
const dataSourcePriceQuery = useCallback(async () => { const dataSourcePriceQuery = useCallback(async () => {
@ -350,7 +425,6 @@ export const useWormholeTransactions = () => {
return { return {
loading, loading,
transfers, transfers,
userTransfers,
totalInUSD: amountInUSD, totalInUSD: amountInUSD,
}; };
}; };

View File

@ -23,6 +23,7 @@ import {
} from '@solana/spl-token-registry'; } from '@solana/spl-token-registry';
export type ENV = export type ENV =
| 'mainnet-beta (Serum)'
| 'mainnet-beta' | 'mainnet-beta'
| 'testnet' | 'testnet'
| 'devnet' | 'devnet'
@ -31,10 +32,15 @@ export type ENV =
export const ENDPOINTS = [ export const ENDPOINTS = [
{ {
name: 'mainnet-beta' as ENV, name: 'mainnet-beta (Serum)' as ENV,
endpoint: 'https://solana-api.projectserum.com/', endpoint: 'https://solana-api.projectserum.com/',
ChainId: ChainId.MainnetBeta, ChainId: ChainId.MainnetBeta,
}, },
{
name: 'mainnet-beta' as ENV,
endpoint: 'https://api.mainnet-beta.solana.com',
ChainId: ChainId.MainnetBeta,
},
{ {
name: 'testnet' as ENV, name: 'testnet' as ENV,
endpoint: clusterApiUrl('testnet'), endpoint: clusterApiUrl('testnet'),
@ -412,6 +418,7 @@ export const sendTransactionWithRetry = async (
commitment: Commitment = 'singleGossip', commitment: Commitment = 'singleGossip',
includesFeePayer: boolean = false, includesFeePayer: boolean = false,
block?: BlockhashAndFeeCalculator, block?: BlockhashAndFeeCalculator,
beforeSend?: () => void,
) => { ) => {
let transaction = new Transaction(); let transaction = new Transaction();
instructions.forEach(instruction => transaction.add(instruction)); instructions.forEach(instruction => transaction.add(instruction));
@ -436,6 +443,10 @@ export const sendTransactionWithRetry = async (
transaction = await wallet.signTransaction(transaction); transaction = await wallet.signTransaction(transaction);
} }
if (beforeSend) {
beforeSend();
}
const { txid, slot } = await sendSignedTransaction({ const { txid, slot } = await sendSignedTransaction({
connection, connection,
signedTransaction: transaction, signedTransaction: transaction,
@ -521,7 +532,7 @@ export async function sendSignedTransaction({
} }
throw new Error(JSON.stringify(simulateResult.err)); throw new Error(JSON.stringify(simulateResult.err));
} }
throw new Error('Transaction failed'); // throw new Error('Transaction failed');
} finally { } finally {
done = true; done = true;
} }

View File

@ -143,7 +143,7 @@ export const PROGRAM_IDS = [
]; ];
export const setProgramIds = (envName: string) => { export const setProgramIds = (envName: string) => {
let instance = PROGRAM_IDS.find(env => env.name === envName); let instance = PROGRAM_IDS.find(env => envName.indexOf(env.name) >= 0);
if (!instance) { if (!instance) {
return; return;
} }