fix: transfer

This commit is contained in:
bartosz-lipinski 2021-03-09 20:34:48 -06:00
parent 39ae89ca8b
commit 730777f732
6 changed files with 485 additions and 154 deletions

View File

@ -1,32 +1,24 @@
import React, { useState } from 'react';
import { NumericInput, programIds } from '@oyster/common';
import { NumericInput } from '@oyster/common';
import { Card, Select } from 'antd';
import './style.less';
import { useEthereum } from '../../contexts';
import { WrappedAssetFactory } from '../../contracts/WrappedAssetFactory';
import { WormholeFactory } from '../../contracts/WormholeFactory';
import { TransferRequestInfo } from '../../models/bridge';
import { TokenDisplay } from '../TokenDisplay';
import BN from 'bn.js';
import { ASSET_CHAIN } from '../../models/bridge/constants';
const { Option } = Select;
export function EthereumInput(props: {
title: string;
hideBalance?: boolean;
balance?: number;
asset?: string;
chain?: ASSET_CHAIN;
setAsset: (asset: string) => void;
setInfo: (info: TransferRequestInfo) => void;
amount?: number | null;
onInputChange: (value: number | null) => void;
onInputChange: (value: number | undefined) => void;
}) {
const [balance, setBalance] = useState<number>(0);
const [lastAmount, setLastAmount] = useState<string>('');
const { tokens, provider } = useEthereum();
const { tokens } = useEthereum();
const renderReserveAccounts = tokens
.filter(t => (t.tags?.indexOf('longList') || -1) < 0)
@ -54,54 +46,6 @@ export function EthereumInput(props: {
);
});
const updateBalance = async (fromAddress: string) => {
props.setAsset(fromAddress);
if (!provider) {
return;
}
const bridgeAddress = programIds().wormhole.bridge;
let signer = provider.getSigner();
let e = WrappedAssetFactory.connect(fromAddress, provider);
let addr = await signer.getAddress();
let balance = await e.balanceOf(addr);
let decimals = await e.decimals();
let symbol = await e.symbol();
let allowance = await e.allowance(addr, bridgeAddress);
let info = {
address: fromAddress,
name: symbol,
balance: balance,
allowance: allowance,
decimals: decimals,
isWrapped: false,
chainID: ASSET_CHAIN.Ethereum,
assetAddress: Buffer.from(fromAddress.slice(2), 'hex'),
mint: '',
};
setBalance(
new BN(info.balance.toString())
.div(new BN(10).pow(new BN(info.decimals)))
.toNumber(),
);
let b = WormholeFactory.connect(bridgeAddress, provider);
let isWrapped = await b.isWrappedAsset(fromAddress);
if (isWrapped) {
info.chainID = await e.assetChain();
info.assetAddress = Buffer.from((await e.assetAddress()).slice(2), 'hex');
info.isWrapped = true;
}
props.setInfo(info);
};
return (
<Card
className="ccy-input from-input"
@ -111,12 +55,12 @@ export function EthereumInput(props: {
<div className="ccy-input-header">
<div className="ccy-input-header-left">{props.title}</div>
{!props.hideBalance && (
{!!props.balance && (
<div
className="ccy-input-header-right"
onClick={e => props.onInputChange && props.onInputChange(balance)}
onClick={() => props.onInputChange && props.onInputChange(props.balance)}
>
Balance: {balance.toFixed(6)}
Balance: {props.balance.toFixed(6)}
</div>
)}
</div>
@ -132,7 +76,7 @@ export function EthereumInput(props: {
}
onChange={(val: string) => {
if (props.onInputChange && parseFloat(val) !== props.amount) {
if (!val || !parseFloat(val)) props.onInputChange(null);
if (!val || !parseFloat(val)) props.onInputChange(undefined);
else props.onInputChange(parseFloat(val));
}
setLastAmount(val);
@ -153,7 +97,7 @@ export function EthereumInput(props: {
placeholder="CCY"
value={props.asset}
onChange={(item: string) => {
updateBalance(item);
props.setAsset(item);
}}
filterOption={(input, option) =>
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0

View File

@ -1,26 +1,21 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Card, notification, Spin, Button } from 'antd';
import { TokenInfo } from '@uniswap/token-lists';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
import { LABELS } from '../../constants';
import React, { useEffect, useState } from 'react';
import { notification, Spin, Button } from 'antd';
import {
contexts,
utils,
ConnectButton,
programIds,
formatAmount,
} from '@oyster/common';
import { useHistory, useLocation } from 'react-router-dom';
import { EthereumInput } from './../Input';
import './style.less';
import { ethers } from 'ethers';
import { ASSET_CHAIN, chainToName } from '../../utils/assets';
import { BigNumber } from 'ethers/utils';
import { Erc20Factory } from '../../contracts/Erc20Factory';
import { ProgressUpdate, transfer, TransferRequest } from '../../models/bridge';
import { ProgressUpdate, toSolana, TransferRequest } from '../../models/bridge';
import { useEthereum } from '../../contexts';
import { TokenDisplay } from './../TokenDisplay';
import { WrappedAssetFactory } from '../../contracts/WrappedAssetFactory';
import { WormholeFactory } from '../../contracts/WormholeFactory';
import BN from 'bn.js';
const { useConnection } = contexts.Connection;
const { useWallet } = contexts.Wallet;
@ -51,23 +46,75 @@ export const Transfer = () => {
toChain: ASSET_CHAIN.Solana,
});
const setAssetInformation = (asset: string) => {
const setAssetInformation = async (asset: string) => {
setRequest({
...request,
asset,
});
};
useEffect(() => {
const asset = request.asset;
if(!asset || asset === request?.info?.address) {
return;
}
(async () => {
if (!provider) {
return;
}
const bridgeAddress = programIds().wormhole.bridge;
let signer = provider.getSigner();
let e = WrappedAssetFactory.connect(asset, provider);
let addr = await signer.getAddress();
let balance = await e.balanceOf(addr);
let decimals = await e.decimals();
let symbol = await e.symbol();
let allowance = await e.allowance(addr, bridgeAddress);
let info = {
address: asset,
name: symbol,
balance: balance,
balanceAsNumber: (new BN(balance.toString())
.div(new BN(10).pow(new BN(decimals - 2)))
.toNumber()) / 100,
allowance: allowance,
decimals: decimals,
isWrapped: false,
chainID: ASSET_CHAIN.Ethereum,
assetAddress: Buffer.from(asset.slice(2), 'hex'),
mint: '',
};
let b = WormholeFactory.connect(bridgeAddress, provider);
let isWrapped = await b.isWrappedAsset(asset);
if (isWrapped) {
info.chainID = await e.assetChain();
info.assetAddress = Buffer.from((await e.assetAddress()).slice(2), 'hex');
info.isWrapped = true;
}
setRequest({
...request,
asset,
info,
});
})();
}, [request, provider])
return (
<>
<div className="exchange-card">
<EthereumInput
title="From Ethereum"
setInfo={info => {
request.info = info;
}}
title={`From ${chainToName(request.from)}`}
asset={request.asset}
chain={request.from}
balance={request.info?.balanceAsNumber || 0 }
setAsset={asset => setAssetInformation(asset)}
amount={request.amount}
onInputChange={amount => {
@ -94,13 +141,9 @@ export const Transfer = () => {
</Button>
<EthereumInput
title="To Solana"
setInfo={info => {
request.info = info;
}}
title={`To ${chainToName(request.toChain)}`}
asset={request.asset}
chain={request.toChain}
hideBalance={true}
setAsset={asset => setAssetInformation(asset)}
amount={request.amount}
onInputChange={amount => {
@ -126,22 +169,24 @@ export const Transfer = () => {
(async () => {
let steps: ProgressUpdate[] = [];
try {
await transfer(
connection,
wallet,
request,
provider,
update => {
if (update.replace) {
steps.pop();
steps = [...steps, update];
} else {
steps = [...steps, update];
}
if(request.toChain === ASSET_CHAIN.Solana) {
await toSolana(
connection,
wallet,
request,
provider,
update => {
if (update.replace) {
steps.pop();
steps = [...steps, update];
} else {
steps = [...steps, update];
}
setActiveSteps(steps);
},
);
setActiveSteps(steps);
},
);
}
} catch {
// TODO...
}

View File

@ -1,6 +1,4 @@
import {
contexts,
utils,
programIds,
WalletAdapter,
getMultipleAccounts,
@ -8,66 +6,26 @@ import {
cache,
TokenAccountParser,
ParsedAccount,
formatNumber,
formatAmount,
createAssociatedTokenAccountInstruction,
} from '@oyster/common';
import { ethers } from 'ethers';
import { ASSET_CHAIN } from '../../utils/assets';
import { ASSET_CHAIN } from '../../../utils/assets';
import { BigNumber } from 'ethers/utils';
import { Erc20Factory } from '../../contracts/Erc20Factory';
import { WormholeFactory } from '../../contracts/WormholeFactory';
import { AssetMeta, createWrappedAssetInstruction } from './meta';
import { bridgeAuthorityKey, wrappedAssetMintKey } from './helpers';
import { Erc20Factory } from '../../../contracts/Erc20Factory';
import { WormholeFactory } from '../../../contracts/WormholeFactory';
import { AssetMeta, createWrappedAssetInstruction } from './../meta';
import { bridgeAuthorityKey, wrappedAssetMintKey } from './../helpers';
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import { AccountInfo, AccountLayout } from '@solana/spl-token';
import { AccountInfo } from '@solana/spl-token';
import { ProgressUpdate, TransferRequest } from './interface';
export interface ProgressUpdate {
message: string;
type: string;
step: number;
group: string;
replace?: boolean;
}
export interface TransferRequestInfo {
name: string;
balance: BigNumber;
decimals: number;
allowance: BigNumber;
isWrapped: boolean;
chainID: number;
assetAddress: Buffer;
mint: string;
}
export interface TransferRequest {
nonce?: number;
signer?: ethers.Signer;
asset?: string;
amount?: number;
amountBN?: BigNumber;
recipient?: Buffer;
info?: TransferRequestInfo;
from?: ASSET_CHAIN;
toChain?: ASSET_CHAIN;
}
// type of updates
// 1. info
// 2. user
// 3. wait (progress bar)
export const transfer = async (
export const fromSolana = async (
connection: Connection,
wallet: WalletAdapter,
request: TransferRequest,
@ -77,7 +35,6 @@ export const transfer = async (
if (!request.asset) {
return;
}
const walletName = 'MetaMask';
request.signer = provider?.getSigner();
@ -384,8 +341,6 @@ export const transfer = async (
);
});
},
//
vaa: async (request: TransferRequest) => {},
};
return steps.transfer(request);

View File

@ -0,0 +1,3 @@
export * from './toSolana';
export * from './fromSolana';
export * from './interface';

View File

@ -0,0 +1,39 @@
import { ethers } from 'ethers';
import { BigNumber } from 'ethers/utils';
import { ASSET_CHAIN } from '../constants';
export interface ProgressUpdate {
message: string;
type: string;
step: number;
group: string;
replace?: boolean;
}
export interface TransferRequestInfo {
address: string;
name: string;
balance: BigNumber;
balanceAsNumber: number;
decimals: number;
allowance: BigNumber;
isWrapped: boolean;
chainID: number;
assetAddress: Buffer;
mint: string;
}
export interface TransferRequest {
nonce?: number;
signer?: ethers.Signer;
asset?: string;
amount?: number;
amountBN?: BigNumber;
recipient?: Buffer;
info?: TransferRequestInfo;
from?: ASSET_CHAIN;
toChain?: ASSET_CHAIN;
}

View File

@ -0,0 +1,345 @@
import {
programIds,
WalletAdapter,
getMultipleAccounts,
sendTransaction,
cache,
TokenAccountParser,
ParsedAccount,
formatAmount,
createAssociatedTokenAccountInstruction,
} from '@oyster/common';
import { ethers } from 'ethers';
import { Erc20Factory } from '../../../contracts/Erc20Factory';
import { WormholeFactory } from '../../../contracts/WormholeFactory';
import { AssetMeta, createWrappedAssetInstruction } from './../meta';
import { bridgeAuthorityKey, wrappedAssetMintKey } from './../helpers';
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import { AccountInfo } from '@solana/spl-token';
import { TransferRequest, ProgressUpdate } from './interface';
export const toSolana = async (
connection: Connection,
wallet: WalletAdapter,
request: TransferRequest,
provider: ethers.providers.Web3Provider,
setProgress: (update: ProgressUpdate) => void,
) => {
if (!request.asset) {
return;
}
const walletName = 'MetaMask';
request.signer = provider?.getSigner();
request.nonce = await provider.getTransactionCount(
request.signer.getAddress(),
'pending',
);
let counter = 0;
// check difference between lock/approve (invoke lock if allowance < amount)
const steps = {
transfer: async (request: TransferRequest) => {
if (!request.info || !request.amount) {
return;
}
request.amountBN = ethers.utils.parseUnits(
formatAmount(request.amount, 9),
request.info.decimals,
);
return steps.prepare(request);
},
// creates wrapped account on solana
prepare: async (request: TransferRequest) => {
if (!request.info || !request.from || !wallet.publicKey) {
return;
}
const group = 'Initiate transfer';
try {
const bridgeId = programIds().wormhole.pubkey;
const authority = await bridgeAuthorityKey(bridgeId);
const meta: AssetMeta = {
decimals: Math.min(request.info?.decimals, 9),
address: request.info?.assetAddress,
chain: request.from,
};
const mintKey = await wrappedAssetMintKey(bridgeId, authority, meta);
const recipientKey =
cache
.byParser(TokenAccountParser)
.map(key => {
let account = cache.get(key) as ParsedAccount<AccountInfo>;
if (account?.info.mint.toBase58() === mintKey.toBase58()) {
return key;
}
return;
})
.find(_ => _) || '';
const recipient: PublicKey = recipientKey
? new PublicKey(recipientKey)
: (
await PublicKey.findProgramAddress(
[
wallet.publicKey.toBuffer(),
programIds().token.toBuffer(),
mintKey.toBuffer(),
],
programIds().associatedToken,
)
)[0];
request.recipient = recipient.toBuffer();
const accounts = await getMultipleAccounts(
connection,
[mintKey.toBase58(), recipient.toBase58()],
'single',
);
const instructions: TransactionInstruction[] = [];
const signers: Account[] = [];
if (!accounts.array[0]) {
// create mint using wormhole instruction
instructions.push(
await createWrappedAssetInstruction(
meta,
bridgeId,
authority,
mintKey,
wallet.publicKey,
),
);
}
if (!accounts.array[1]) {
createAssociatedTokenAccountInstruction(
instructions,
recipient,
wallet.publicKey,
wallet.publicKey,
mintKey,
);
}
if (instructions.length > 0) {
setProgress({
message: 'Waiting for Solana approval...',
type: 'user',
group,
step: counter++,
});
const tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
}
} catch (err) {
setProgress({
message: `Couldn't create Solana account!`,
type: 'error',
group,
step: counter++,
});
throw err;
}
return steps.approve(request);
},
// approves assets for transfer
approve: async (request: TransferRequest) => {
if (!request.amountBN || !request.asset || !request.signer) {
return;
}
const group = 'Approve assets';
try {
if (request.info?.allowance.lt(request.amountBN)) {
let e = Erc20Factory.connect(request.asset, request.signer);
setProgress({
message: `Waiting for ${walletName} approval`,
type: 'user',
group,
step: counter++,
});
let res = await e.approve(
programIds().wormhole.bridge,
request.amountBN,
);
setProgress({
message: 'Waiting for ETH transaction to be minted...',
type: 'wait',
group,
step: counter++,
});
await res.wait(1);
setProgress({
message: 'Approval on ETH succeeded!',
type: 'done',
group,
step: counter++,
});
} else {
setProgress({
message: 'Already approved on ETH!',
type: 'done',
group,
step: counter++,
});
}
} catch (err) {
setProgress({
message: 'Approval failed!',
type: 'error',
group,
step: counter++,
});
throw err;
}
return steps.lock(request);
},
// locks assets in the bridge
lock: async (request: TransferRequest) => {
if (
!request.amountBN ||
!request.asset ||
!request.signer ||
!request.recipient ||
!request.toChain ||
!request.info ||
!request.nonce
) {
return;
}
let group = 'Lock assets';
try {
let wh = WormholeFactory.connect(
programIds().wormhole.bridge,
request.signer,
);
setProgress({
message: `Waiting for ${walletName} transfer approval`,
type: 'user',
group,
step: counter++,
});
let res = await wh.lockAssets(
request.asset,
request.amountBN,
request.recipient,
request.toChain,
request.nonce,
false,
);
setProgress({
message: 'Waiting for ETH transaction to be minted...',
type: 'wait',
group,
step: counter++,
});
await res.wait(1);
setProgress({
message: 'Transfer on ETH succeeded!',
type: 'done',
group,
step: counter++,
});
} catch (err) {
setProgress({
message: 'Transfer failed!',
type: 'error',
group,
step: counter++,
});
throw err;
}
return steps.wait(request);
},
wait: async (request: TransferRequest) => {
let startBlock = provider.blockNumber;
let completed = false;
let group = 'Finalizing transfer';
const ethConfirmationMessage = (current: number) =>
`Awaiting ETH confirmations: ${current} out of 15`;
setProgress({
message: ethConfirmationMessage(0),
type: 'wait',
step: counter++,
group,
});
let blockHandler = (blockNumber: number) => {
let passedBlocks = blockNumber - startBlock;
const isLast = passedBlocks === 14;
if (passedBlocks < 15) {
setProgress({
message: ethConfirmationMessage(passedBlocks),
type: isLast ? 'done' : 'wait',
step: counter++,
group,
replace: passedBlocks > 0,
});
if (isLast) {
setProgress({
message: 'Awaiting completion on Solana...',
type: 'wait',
group,
step: counter++,
});
}
} else if (!completed) {
provider.removeListener('block', blockHandler);
}
};
provider.on('block', blockHandler);
return new Promise<void>((resolve, reject) => {
if (!request.recipient) {
return;
}
let accountChangeListener = connection.onAccountChange(
new PublicKey(request.recipient),
() => {
if (completed) return;
completed = true;
provider.removeListener('block', blockHandler);
connection.removeAccountChangeListener(accountChangeListener);
setProgress({
message: 'Transfer completed on Solana',
type: 'info',
group,
step: counter++,
});
resolve();
},
'single',
);
});
},
};
return steps.transfer(request);
};