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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Button, Table, Tabs, notification } from 'antd';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import './index.less';
@ -34,11 +34,7 @@ const { TabPane } = Tabs;
export const RecentTransactionsTable = (props: {
showUserTransactions?: boolean;
}) => {
const {
loading: loadingTransfers,
transfers,
userTransfers,
} = useWormholeTransactions();
const { loading: loadingTransfers, transfers } = useWormholeTransactions();
const { provider } = useEthereum();
const bridge = useBridge();
@ -154,221 +150,197 @@ 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 = [
...baseColumns,
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render(text: string, record: any) {
const status =
completedVAAs.indexOf(record.txhash) > 0
? 'Completed'
: record.status;
return {
props: { style: {} },
children: (
<>
<span className={`${record.status?.toLowerCase()}`}>
{status}
</span>
{status === 'Failed' ? (
<Button
onClick={() => {
const NotificationContent = () => {
const [activeSteps, setActiveSteps] = useState<
ProgressUpdate[]
>([]);
let counter = 0;
useEffect(() => {
(async () => {
const signer = provider?.getSigner();
if (!signer || !bridge) {
setActiveSteps([
...activeSteps,
{
message: 'Connect your Ethereum Wallet',
type: 'error',
group: 'error',
step: counter++,
},
]);
} else {
const lockup = record.lockup;
let vaa = lockup.vaa;
for (let i = vaa.length; i > 0; i--) {
if (vaa[i] == 0xff) {
vaa = vaa.slice(0, i);
break;
}
}
let signatures = await bridge.fetchSignatureStatus(
lockup.signatureAccount,
);
let sigData = Buffer.of(
...signatures.reduce(
(previousValue, currentValue) => {
previousValue.push(currentValue.index);
previousValue.push(...currentValue.signature);
return previousValue;
const userColumns = useMemo(
() => [
...baseColumns,
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render(text: string, record: any) {
const status =
completedVAAs.indexOf(record.txhash) > 0
? 'Completed'
: record.status;
return {
props: { style: {} },
children: (
<>
<span className={`${record.status?.toLowerCase()}`}>
{status}
</span>
{status === 'Failed' ? (
<Button
onClick={() => {
const NotificationContent = () => {
const [activeSteps, setActiveSteps] = useState<
ProgressUpdate[]
>([]);
let counter = 0;
useEffect(() => {
(async () => {
const signer = provider?.getSigner();
if (!signer || !bridge) {
setActiveSteps([
...activeSteps,
{
message: 'Connect your Ethereum Wallet',
type: 'error',
group: 'error',
step: counter++,
},
new Array<number>(),
),
);
]);
} else {
const lockup = record.lockup;
let vaa = lockup.vaa;
for (let i = vaa.length; i > 0; i--) {
if (vaa[i] == 0xff) {
vaa = vaa.slice(0, i);
break;
}
}
let signatures = await bridge.fetchSignatureStatus(
lockup.signatureAccount,
);
let sigData = Buffer.of(
...signatures.reduce(
(previousValue, currentValue) => {
previousValue.push(currentValue.index);
previousValue.push(
...currentValue.signature,
);
vaa = Buffer.concat([
vaa.slice(0, 5),
Buffer.of(signatures.length),
sigData,
vaa.slice(6),
]);
let wh = WormholeFactory.connect(
programIds().wormhole.bridge,
signer,
);
let group = 'Finalizing transfer';
setActiveSteps([
...activeSteps,
{
message: 'Sign the claim...',
type: 'wait',
group,
step: counter++,
},
]);
let tx = await wh.submitVAA(vaa);
setActiveSteps([
...activeSteps,
{
message:
'Waiting for tokens unlock to be mined...',
type: 'wait',
group,
step: counter++,
},
]);
await tx.wait(1);
setActiveSteps([
...activeSteps,
{
message: 'Execution of VAA succeeded',
type: 'done',
group,
step: counter++,
},
]);
}
})();
}, [setActiveSteps]);
return previousValue;
},
new Array<number>(),
),
);
return (
<div>
<div
style={{
textAlign: 'left',
display: 'flex',
flexDirection: 'column',
}}
>
{(() => {
let group = '';
return activeSteps.map((step, i) => {
let prevGroup = group;
group = step.group;
let newGroup = prevGroup !== group;
return (
<>
{newGroup && <span>{group}</span>}
<span style={{ marginLeft: 15 }}>
{typeToIcon(
step.type,
activeSteps.length - 1 === i,
)}{' '}
{step.message}
</span>
</>
);
});
})()}
vaa = Buffer.concat([
vaa.slice(0, 5),
Buffer.of(signatures.length),
sigData,
vaa.slice(6),
]);
let wh = WormholeFactory.connect(
programIds().wormhole.bridge,
signer,
);
let group = 'Finalizing transfer';
setActiveSteps([
...activeSteps,
{
message: 'Sign the claim...',
type: 'wait',
group,
step: counter++,
},
]);
let tx = await wh.submitVAA(vaa);
setActiveSteps([
...activeSteps,
{
message:
'Waiting for tokens unlock to be mined... (Up to few min.)',
type: 'wait',
group,
step: counter++,
},
]);
await tx.wait(1);
setActiveSteps([
...activeSteps,
{
message: 'Execution of VAA succeeded',
type: 'done',
group,
step: counter++,
},
]);
setCompletedVAAs([
...completedVAAs,
record.txhash,
]);
}
})();
}, [setActiveSteps]);
return (
<div>
<div
style={{
textAlign: 'left',
display: 'flex',
flexDirection: 'column',
}}
>
{(() => {
let group = '';
return activeSteps.map((step, i) => {
let prevGroup = group;
group = step.group;
let newGroup = prevGroup !== group;
return (
<>
{newGroup && <span>{group}</span>}
<span style={{ marginLeft: 15 }}>
{typeToIcon(
step.type,
activeSteps.length - 1 === i,
)}{' '}
{step.message}
</span>
</>
);
});
})()}
</div>
</div>
</div>
);
};
);
};
notification.open({
message: '',
duration: 0,
placement: 'bottomLeft',
description: <NotificationContent />,
className: 'custom-class',
style: {
width: 500,
},
});
}}
shape="circle"
size="large"
type="text"
style={{ color: '#547595', fontSize: '18px' }}
title={'Retry Transaction'}
icon={<SyncOutlined />}
/>
) : null}
</>
),
};
notification.open({
message: '',
duration: 0,
placement: 'bottomLeft',
description: <NotificationContent />,
className: 'custom-class',
style: {
width: 500,
},
});
}}
shape="circle"
size="large"
type="text"
style={{ color: '#547595', fontSize: '18px' }}
title={'Retry Transaction'}
icon={<SyncOutlined />}
/>
) : null}
</>
),
};
},
},
},
];
],
[completedVAAs, bridge, provider],
);
return (
<div id={'recent-tx-container'}>
<div className={'home-subtitle'} style={{ marginBottom: '70px' }}>
Transactions
My Recent Transactions
</div>
<Tabs defaultActiveKey="1" centered>
<TabPane tab="Recent Transactions" key="1">
<Table
scroll={{
scrollToFirstRowOnChange: false,
x: 900,
}}
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}
loading={loadingTransfers}
/>
</TabPane>
</Tabs>
<Table
scroll={{
scrollToFirstRowOnChange: false,
x: 900,
}}
dataSource={transfers.sort((a, b) => b.date - a.date)}
columns={userColumns}
loading={loadingTransfers}
/>
</div>
);
};

View File

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

View File

@ -1,27 +1,33 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
notify,
programIds,
useConnection,
useConnectionConfig,
programIds,
notify,
useWallet,
ParsedAccountBase,
} from '@oyster/common';
import {
WORMHOLE_PROGRAM_ID,
POSTVAA_INSTRUCTION,
TRANSFER_ASSETS_OUT_INSTRUCTION,
WORMHOLE_PROGRAM_ID,
} from '../utils/ids';
import { ASSET_CHAIN } from '../utils/assets';
import { useEthereum } from '../contexts';
import {
AccountInfo,
Connection,
ParsedAccountData,
PartiallyDecodedInstruction,
PublicKey,
RpcResponseAndContext,
} from '@solana/web3.js';
import {
bridgeAuthorityKey,
LockupStatus,
LockupWithStatus,
SolanaBridge,
TransferOutProposalLayout,
WormholeFactory,
} from '@solana/bridge-sdk';
import bs58 from 'bs58';
@ -31,11 +37,10 @@ import {
useCoingecko,
} from '../contexts/coingecko';
import { BigNumber } from 'bignumber.js';
import { WormholeFactory } from '@solana/bridge-sdk';
import { ethers } from 'ethers';
import { useBridge } from '../contexts/bridge';
import { SolanaBridge } from '@solana/bridge-sdk';
import BN from 'bn.js';
import { keccak256 } from 'ethers/utils';
type WrappedTransferMeta = {
chain: number;
@ -59,6 +64,86 @@ type 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 (
authorityKey: PublicKey,
connection: Connection,
@ -238,12 +323,11 @@ export const useWormholeTransactions = () => {
const { tokenMap: ethTokens } = useEthereum();
const { tokenMap } = useConnectionConfig();
const { coinList } = useCoingecko();
const { wallet, connected: walletConnected } = useWallet();
const { wallet } = useWallet();
const bridge = useBridge();
const [loading, setLoading] = useState<boolean>(true);
const [transfers, setTransfers] = useState<WrappedTransferMeta[]>([]);
const [userTransfers, setUserTransfers] = useState<WrappedTransferMeta[]>([]);
const [amountInUSD, setAmountInUSD] = useState<number>(0);
useEffect(() => {
@ -263,26 +347,17 @@ export const useWormholeTransactions = () => {
(window as any).ethereum,
);
// query wrapped assets that were imported to solana from other chains
queryWrappedMetaTransactions(
queryOwnWrappedMetaTransactions(
authorityKey,
connection,
setTransfers,
provider,
bridge,
wallet?.publicKey,
).then(() => setLoading(false));
}
})();
}, [connection, setTransfers]);
useEffect(() => {
if (transfers && walletConnected && wallet?.publicKey) {
setUserTransfers(
transfers.filter(t => {
return t.owner === wallet?.publicKey?.toBase58();
}),
);
}
}, [wallet, walletConnected, transfers]);
}, [connection, setTransfers, wallet?.publicKey]);
const coingeckoTimer = useRef<number>(0);
const dataSourcePriceQuery = useCallback(async () => {
@ -350,7 +425,6 @@ export const useWormholeTransactions = () => {
return {
loading,
transfers,
userTransfers,
totalInUSD: amountInUSD,
};
};

View File

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

View File

@ -143,7 +143,7 @@ export const PROGRAM_IDS = [
];
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) {
return;
}