bridge_ui: set target address in state

Change-Id: Ie2f87582ffdc8da53ccc0f34721c3985d7807933
This commit is contained in:
Evan Gray 2021-08-25 01:55:26 -04:00
parent 1f214029f5
commit c33f3c0cb9
10 changed files with 194 additions and 71 deletions

View File

@ -29,6 +29,7 @@
"@solana/wallet-base": "^0.0.1",
"@solana/web3.js": "^1.22.0",
"@terra-money/wallet-provider": "^1.4.0-alpha.1",
"bech32": "^1.1.4",
"ethers": "^5.4.1",
"js-base64": "^3.6.1",
"notistack": "^1.0.10",
@ -57,6 +58,9 @@
"@improbable-eng/grpc-web": "^0.14.0",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^1.8.10",
"@terra-money/wallet-provider": "^1.2.4",
"js-base64": "^3.6.1",
"protobufjs": "^6.11.2",
"rxjs": "^7.3.0"
},
@ -65,6 +69,7 @@
"@typechain/ethers-v5": "^7.0.1",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"@types/react": "^17.0.19",
"copy-dir": "^1.3.0",
"ethers": "^5.4.4",
"prettier": "^2.3.2",
@ -36739,6 +36744,7 @@
"resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz",
"integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/core": "^7.0.0-beta.39",
"@babel/traverse": "^7.0.0-beta.39",
@ -36752,6 +36758,7 @@
"resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz",
"integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==",
"dev": true,
"peer": true,
"bin": {
"babylon": "bin/babylon.js"
},
@ -37587,6 +37594,7 @@
"resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz",
"integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==",
"dev": true,
"peer": true,
"engines": {
"node": "*"
}
@ -37596,6 +37604,7 @@
"resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz",
"integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.0.0-beta.36",
"long": "^3.2.0",
@ -37615,6 +37624,7 @@
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
"integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.6"
}
@ -41022,11 +41032,15 @@
"@openzeppelin/contracts": "^4.2.0",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^1.8.10",
"@terra-money/wallet-provider": "^1.2.4",
"@typechain/ethers-v5": "^7.0.1",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"@types/react": "^17.0.19",
"copy-dir": "^1.3.0",
"ethers": "^5.4.4",
"js-base64": "^3.6.1",
"prettier": "^2.3.2",
"protobufjs": "^6.11.2",
"rxjs": "^7.3.0",
@ -48382,7 +48396,6 @@
"dev": true,
"optional": true,
"requires": {
"bitcore-lib": "^8.25.10",
"unorm": "^1.4.1"
}
},
@ -69246,6 +69259,7 @@
"resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz",
"integrity": "sha512-Fq1+nu43ybsjSnBquLrW/cULmKs61qbv9k8ep13QUe0nABBezMoNAA+j6QY66MW0/eoDVDp1rjXDqQ2VKyS/Xg==",
"dev": true,
"peer": true,
"requires": {
"@babel/core": "^7.0.0-beta.39",
"@babel/traverse": "^7.0.0-beta.39",
@ -69258,7 +69272,8 @@
"version": "7.0.0-beta.47",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz",
"integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==",
"dev": true
"dev": true,
"peer": true
}
}
},
@ -69268,8 +69283,7 @@
"integrity": "sha512-R4s75XH+o8qM+WaRrAU9S2rbAMDzob18/S3V8R9ZoFpZkPWLAohWWlzWAp1ybeTkOuuku/X1zJtxiV0pBYxZww==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0",
"wasm-dce": "^1.0.0"
"loader-utils": "^1.1.0"
},
"dependencies": {
"json5": {
@ -69991,13 +70005,15 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/webassembly-floating-point-hex-parser/-/webassembly-floating-point-hex-parser-0.1.2.tgz",
"integrity": "sha512-TUf1H++8U10+stJbFydnvrpG5Sznz5Rilez/oZlV5zI0C/e4cSxd8rALAJ8VpTvjVWxLmL3SVSJUK6Ap9AoiNg==",
"dev": true
"dev": true,
"peer": true
},
"webassembly-interpreter": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/webassembly-interpreter/-/webassembly-interpreter-0.0.30.tgz",
"integrity": "sha512-+Jdy2piEvz9T5j751mOE8+rBO12p+nNW6Fg4kJZ+zP1oUfsm+151sbAbM8AFxWTURmWCGP+r8Lxwfv3pzN1bCQ==",
"dev": true,
"peer": true,
"requires": {
"@babel/code-frame": "^7.0.0-beta.36",
"long": "^3.2.0",
@ -70008,7 +70024,8 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
"integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
"dev": true
"dev": true,
"peer": true
}
}
},

View File

@ -23,6 +23,7 @@
"@solana/wallet-base": "^0.0.1",
"@solana/web3.js": "^1.22.0",
"@terra-money/wallet-provider": "^1.4.0-alpha.1",
"bech32": "^1.1.4",
"ethers": "^5.4.1",
"js-base64": "^3.6.1",
"notistack": "^1.0.10",

View File

@ -63,7 +63,7 @@ function Source() {
</TextField>
<KeyAndBalance chainId={sourceChain} />
<TextField
placeholder="Asset"
label="Asset"
fullWidth
className={classes.transferField}
value={sourceAsset}

View File

@ -75,7 +75,7 @@ function Source() {
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
{/* TODO: token list for eth, check own */}
<TextField
placeholder="Asset"
label="Asset"
fullWidth
className={classes.transferField}
value={sourceAsset}
@ -83,7 +83,7 @@ function Source() {
disabled={shouldLockFields}
/>
<TextField
placeholder="Amount"
label="Amount"
type="number"
fullWidth
className={classes.transferField}

View File

@ -3,11 +3,13 @@ import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
import { PublicKey } from "@solana/web3.js";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
import {
selectTransferIsSourceAssetWormholeWrapped,
selectTransferIsTargetComplete,
selectTransferShouldLockFields,
selectTransferSourceChain,
selectTransferTargetAddressHex,
selectTransferTargetAsset,
selectTransferTargetBalanceString,
selectTransferTargetChain,
@ -32,6 +34,7 @@ function Target() {
[sourceChain]
);
const targetChain = useSelector(selectTransferTargetChain);
const targetAddressHex = useSelector(selectTransferTargetAddressHex); // TODO: make readable
const targetAsset = useSelector(selectTransferTargetAsset);
const isSourceAssetWormholeWrapped = useSelector(
selectTransferIsSourceAssetWormholeWrapped
@ -47,6 +50,7 @@ function Target() {
const uiAmountString = useSelector(selectTransferTargetBalanceString);
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields);
useSyncTargetAddress(!shouldLockFields);
const handleTargetChange = useCallback(
(event) => {
dispatch(setTargetChain(event.target.value));
@ -76,7 +80,14 @@ function Target() {
</TextField>
<KeyAndBalance chainId={targetChain} balance={uiAmountString} />
<TextField
placeholder="Asset"
label="Address"
fullWidth
className={classes.transferField}
value={targetAddressHex || ""}
disabled={true}
/>
<TextField
label="Asset"
fullWidth
className={classes.transferField}
value={readableTargetAsset}

View File

@ -11,22 +11,17 @@ import {
transferFromEth,
transferFromSolana,
} from "@certusone/wormhole-sdk";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { WalletContextState } from "@solana/wallet-adapter-react";
import { Connection, PublicKey } from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import {
ConnectedWallet,
useConnectedWallet,
} from "@terra-money/wallet-provider";
import { Signer } from "ethers";
import { arrayify, parseUnits, zeroPad } from "ethers/lib/utils";
import { parseUnits, zeroPad } from "ethers/lib/utils";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
@ -40,9 +35,7 @@ import {
selectTransferSourceAsset,
selectTransferSourceChain,
selectTransferSourceParsedTokenAccount,
selectTransferTargetAsset,
selectTransferTargetChain,
selectTransferTargetParsedTokenAccount,
} from "../store/selectors";
import { setIsSending, setSignedVAAHex } from "../store/transferSlice";
import { hexToUint8Array, uint8ArrayToHex } from "../utils/array";
@ -56,6 +49,7 @@ import {
} from "../utils/consts";
import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
import { signSendAndConfirm } from "../utils/solana";
import useTransferTargetAddressHex from "./useTransferTargetAddress";
async function eth(
dispatch: any,
@ -104,8 +98,8 @@ async function solana(
mintAddress: string,
amount: string,
decimals: number,
targetAddressStr: string,
targetChain: ChainId,
targetAddress: Uint8Array,
originAddressStr?: string,
originChain?: ChainId
) {
@ -114,7 +108,6 @@ async function solana(
//TODO: check if token attestation exists on the target chain
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
const amountParsed = parseUnits(amount, decimals).toBigInt();
const originAddress = originAddressStr
? zeroPad(hexToUint8Array(originAddressStr), 32)
@ -164,8 +157,8 @@ async function terra(
wallet: ConnectedWallet,
asset: string,
amount: string,
targetAddressStr: string,
targetChain: ChainId
targetChain: ChainId,
targetAddress: Uint8Array
) {
dispatch(setIsSending(true));
try {
@ -180,7 +173,7 @@ async function terra(
asset: asset,
amount: amount,
recipient_chain: targetChain,
recipient: targetAddressStr,
recipient: targetAddress,
fee: 1000,
nonce: 0,
},
@ -221,11 +214,11 @@ export function useHandleTransfer() {
const originAsset = useSelector(selectTransferOriginAsset);
const amount = useSelector(selectTransferAmount);
const targetChain = useSelector(selectTransferTargetChain);
const targetAsset = useSelector(selectTransferTargetAsset);
const targetAddress = useTransferTargetAddressHex();
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const isSending = useSelector(selectTransferIsSending);
const isSendComplete = useSelector(selectTransferIsSendComplete);
const { signer, signerAddress } = useEthereumProvider();
const { signer } = useEthereumProvider();
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const terraWallet = useConnectedWallet();
@ -234,37 +227,7 @@ export function useHandleTransfer() {
);
const sourceTokenPublicKey = sourceParsedTokenAccount?.publicKey;
const decimals = sourceParsedTokenAccount?.decimals;
const targetParsedTokenAccount = useSelector(
selectTransferTargetParsedTokenAccount
);
const disabled = !isTargetComplete || isSending || isSendComplete;
// TODO: we probably shouldn't get here if we don't have this public key
// TODO: also this is just for solana... send help(ers)
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
// TODO: AVOID THIS DANGEROUS CACOPHONY
const tpkRef = useRef<undefined | Uint8Array>(undefined);
useEffect(() => {
(async () => {
if (targetChain === CHAIN_ID_SOLANA) {
tpkRef.current = targetTokenAccountPublicKey
? zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32) // use the target's TokenAccount if it exists
: solPK && targetAsset // otherwise, use the associated token account (which we create in the case it doesn't exist)
? zeroPad(
(
await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
new PublicKey(targetAsset),
solPK
)
).toBytes(),
32
)
: undefined;
} else tpkRef.current = undefined;
})();
}, [targetChain, solPK, targetAsset, targetTokenAccountPublicKey]);
// TODO: dynamically get "to" wallet
const handleTransferClick = useCallback(() => {
// TODO: we should separate state for transaction vs fetching vaa
// TODO: more generic way of calling these
@ -272,7 +235,7 @@ export function useHandleTransfer() {
sourceChain === CHAIN_ID_ETH &&
!!signer &&
decimals !== undefined &&
!!tpkRef.current
!!targetAddress
) {
eth(
dispatch,
@ -282,14 +245,14 @@ export function useHandleTransfer() {
decimals,
amount,
targetChain,
tpkRef.current
targetAddress
);
} else if (
sourceChain === CHAIN_ID_SOLANA &&
!!solanaWallet &&
!!solPK &&
!!sourceTokenPublicKey &&
!!signerAddress &&
!!targetAddress &&
decimals !== undefined
) {
solana(
@ -299,10 +262,10 @@ export function useHandleTransfer() {
solPK.toString(),
sourceTokenPublicKey,
sourceAsset,
amount, //TODO: avoid decimals, pass in parsed amount
amount,
decimals,
signerAddress,
targetChain,
targetAddress,
originAsset,
originChain
);
@ -310,7 +273,7 @@ export function useHandleTransfer() {
sourceChain === CHAIN_ID_TERRA &&
!!terraWallet &&
decimals !== undefined &&
!!signerAddress
!!targetAddress
) {
terra(
dispatch,
@ -318,8 +281,8 @@ export function useHandleTransfer() {
terraWallet,
sourceAsset,
amount,
signerAddress, // TODO: only works for Eth
targetChain
targetChain,
targetAddress
);
} else {
// enqueueSnackbar("Transfers from this chain are not yet supported", {
@ -331,7 +294,6 @@ export function useHandleTransfer() {
enqueueSnackbar,
sourceChain,
signer,
signerAddress,
solanaWallet,
solPK,
terraWallet,
@ -340,6 +302,7 @@ export function useHandleTransfer() {
amount,
decimals,
targetChain,
targetAddress,
originAsset,
originChain,
]);

View File

@ -0,0 +1,110 @@
import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
} from "@certusone/wormhole-sdk";
import { arrayify, zeroPad } from "@ethersproject/bytes";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import { useConnectedWallet } from "@terra-money/wallet-provider";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import {
selectTransferTargetAsset,
selectTransferTargetChain,
selectTransferTargetParsedTokenAccount,
} from "../store/selectors";
import { setTargetAddressHex } from "../store/transferSlice";
import { uint8ArrayToHex } from "../utils/array";
import bech32 from "bech32";
function useSyncTargetAddress(shouldFire: boolean) {
const dispatch = useDispatch();
const targetChain = useSelector(selectTransferTargetChain);
const { signerAddress } = useEthereumProvider();
const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey;
const targetAsset = useSelector(selectTransferTargetAsset);
const targetParsedTokenAccount = useSelector(
selectTransferTargetParsedTokenAccount
);
const targetTokenAccountPublicKey = targetParsedTokenAccount?.publicKey;
const terraWallet = useConnectedWallet();
useEffect(() => {
if (shouldFire) {
let cancelled = false;
if (targetChain === CHAIN_ID_ETH && signerAddress) {
dispatch(
setTargetAddressHex(
uint8ArrayToHex(zeroPad(arrayify(signerAddress), 32))
)
);
}
// TODO: have the user explicitly select an account on solana
else if (targetChain === CHAIN_ID_SOLANA && targetTokenAccountPublicKey) {
// use the target's TokenAccount if it exists
dispatch(
setTargetAddressHex(
uint8ArrayToHex(
zeroPad(new PublicKey(targetTokenAccountPublicKey).toBytes(), 32)
)
)
);
} else if (targetChain === CHAIN_ID_SOLANA && solPK && targetAsset) {
// otherwise, use the associated token account (which we create in the case it doesn't exist)
(async () => {
const associatedTokenAccount = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
new PublicKey(targetAsset),
solPK
);
if (!cancelled) {
dispatch(
setTargetAddressHex(
uint8ArrayToHex(zeroPad(associatedTokenAccount.toBytes(), 32))
)
);
}
})();
} else if (
targetChain === CHAIN_ID_TERRA &&
terraWallet &&
terraWallet.walletAddress
) {
dispatch(
setTargetAddressHex(
uint8ArrayToHex(
zeroPad(
new Uint8Array(bech32.decode(terraWallet.walletAddress).words),
32
)
)
)
);
} else {
dispatch(setTargetAddressHex(undefined));
}
return () => {
cancelled = true;
};
}
}, [
dispatch,
shouldFire,
targetChain,
signerAddress,
solPK,
targetAsset,
targetTokenAccountPublicKey,
terraWallet,
]);
}
export default useSyncTargetAddress;

View File

@ -0,0 +1,13 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { selectTransferTargetAddressHex } from "../store/selectors";
import { hexToUint8Array } from "../utils/array";
export default function useTransferTargetAddressHex() {
const targetAddressHex = useSelector(selectTransferTargetAddressHex);
const targetAddress = useMemo(
() => (targetAddressHex ? hexToUint8Array(targetAddressHex) : undefined),
[targetAddressHex]
);
return targetAddress;
}

View File

@ -57,6 +57,8 @@ export const selectTransferSourceBalanceString = (state: RootState) =>
export const selectTransferAmount = (state: RootState) => state.transfer.amount;
export const selectTransferTargetChain = (state: RootState) =>
state.transfer.targetChain;
export const selectTransferTargetAddressHex = (state: RootState) =>
state.transfer.targetAddressHex;
export const selectTransferTargetAsset = (state: RootState) =>
state.transfer.targetAsset;
export const selectTransferTargetParsedTokenAccount = (state: RootState) =>
@ -96,11 +98,8 @@ export const selectTransferIsTargetComplete = (state: RootState) =>
!!state.transfer.targetChain &&
!!state.transfer.targetAsset &&
(state.transfer.targetChain !== CHAIN_ID_ETH ||
state.transfer.targetAsset !== ethers.constants.AddressZero); //&&
// Associated Token Account exists
// (state.transfer.targetChain !== CHAIN_ID_SOLANA ||
// (!!state.transfer.targetParsedTokenAccount &&
// !!state.transfer.targetParsedTokenAccount.publicKey));
state.transfer.targetAsset !== ethers.constants.AddressZero) &&
!!state.transfer.targetAddressHex;
export const selectTransferIsSendComplete = (state: RootState) =>
!!selectTransferSignedVAAHex(state);
export const selectTransferShouldLockFields = (state: RootState) =>

View File

@ -34,6 +34,7 @@ export interface TransferState {
sourceParsedTokenAccount: ParsedTokenAccount | undefined;
amount: string;
targetChain: ChainId;
targetAddressHex: string | undefined;
targetAsset: string | null | undefined;
targetParsedTokenAccount: ParsedTokenAccount | undefined;
signedVAAHex: string | undefined;
@ -51,6 +52,7 @@ const initialState: TransferState = {
originAsset: undefined,
amount: "",
targetChain: CHAIN_ID_ETH,
targetAddressHex: undefined,
targetAsset: undefined,
targetParsedTokenAccount: undefined,
signedVAAHex: undefined,
@ -86,6 +88,7 @@ export const transferSlice = createSlice({
}
if (state.targetChain === action.payload) {
state.targetChain = prevSourceChain;
state.targetAddressHex = undefined;
}
},
setSourceAsset: (state, action: PayloadAction<string>) => {
@ -117,6 +120,8 @@ export const transferSlice = createSlice({
setTargetChain: (state, action: PayloadAction<ChainId>) => {
const prevTargetChain = state.targetChain;
state.targetChain = action.payload;
state.targetAddressHex = undefined;
// targetAsset is handled by useFetchTargetAsset
if (state.sourceChain === action.payload) {
state.sourceChain = prevTargetChain;
state.activeStep = 0;
@ -132,6 +137,9 @@ export const transferSlice = createSlice({
}
}
},
setTargetAddressHex: (state, action: PayloadAction<string | undefined>) => {
state.targetAddressHex = action.payload;
},
setTargetAsset: (
state,
action: PayloadAction<string | null | undefined>
@ -169,6 +177,7 @@ export const {
setSourceParsedTokenAccount,
setAmount,
setTargetChain,
setTargetAddressHex,
setTargetAsset,
setTargetParsedTokenAccount,
setSignedVAAHex,