bridge_ui: attestFrom eth and sol

Change-Id: I2eed25b47bcac8891e059d0e11aa624aba802c47
This commit is contained in:
Evan Gray 2021-08-05 14:51:23 -04:00 committed by Hendrik Hofstadt
parent 8444363ac8
commit 012c30b30b
7 changed files with 323 additions and 68 deletions

View File

@ -7,13 +7,16 @@ import {
TextField,
Typography,
} from "@material-ui/core";
import { ethers } from "ethers";
import { useCallback, useState } from "react";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import useEthereumBalance from "../hooks/useEthereumBalance";
import useSolanaBalance from "../hooks/useSolanaBalance";
import useWrappedAsset from "../hooks/useWrappedAsset";
import attestFrom, {
attestFromEth,
attestFromSolana,
} from "../utils/attestFrom";
import {
ChainId,
CHAINS,
@ -109,16 +112,29 @@ function Transfer() {
decimals: solDecimals,
uiAmount: solBalance,
} = useSolanaBalance(assetAddress, solPK, fromChain === CHAIN_ID_SOLANA);
const { isLoading: isCheckingWrapped, wrappedAsset } = useWrappedAsset(
toChain,
fromChain,
assetAddress,
provider
);
console.log(isCheckingWrapped, wrappedAsset);
// TODO: make a helper function for this
const isWrapped = true;
// wrappedAsset && wrappedAsset !== ethers.constants.AddressZero;
const {
isLoading: isCheckingWrapped,
isWrapped,
wrappedAsset,
} = useWrappedAsset(toChain, fromChain, assetAddress, provider);
console.log(isCheckingWrapped, isWrapped, wrappedAsset);
const handleAttestClick = useCallback(() => {
// TODO: more generic way of calling these
if (attestFrom[fromChain]) {
if (
fromChain === CHAIN_ID_ETH &&
attestFrom[fromChain] === attestFromEth
) {
attestFromEth(provider, assetAddress);
}
if (
fromChain === CHAIN_ID_SOLANA &&
attestFrom[fromChain] === attestFromSolana
) {
attestFromSolana(wallet, solPK?.toString(), assetAddress, solDecimals);
}
}
}, [fromChain, provider, wallet, solPK, assetAddress, solDecimals]);
// TODO: dynamically get "to" wallet
const handleTransferClick = useCallback(() => {
// TODO: more generic way of calling these
@ -169,12 +185,18 @@ function Transfer() {
fromChain === CHAIN_ID_ETH
);
const balance = Number(ethBalance) || solBalance;
const isAttestImplemented = !!attestFrom[fromChain];
const isTransferImplemented = !!transferFrom[fromChain];
const isProviderConnected = !!provider;
const isRecipientAvailable = !!solPK;
const isAddressDefined = !!assetAddress;
const isAmountPositive = Number(amount) > 0; // TODO: this needs per-chain, bn parsing
const isBalanceAtLeastAmount = balance >= Number(amount); // TODO: ditto
const canAttemptAttest =
isAttestImplemented &&
isProviderConnected &&
isRecipientAvailable &&
isAddressDefined;
const canAttemptTransfer =
isTransferImplemented &&
isProviderConnected &&
@ -267,7 +289,8 @@ function Transfer() {
<Button
color="primary"
variant="contained"
disabled={isCheckingWrapped}
disabled={isCheckingWrapped || !canAttemptAttest}
onClick={handleAttestClick}
className={classes.transferButton}
>
Attest
@ -286,13 +309,25 @@ function Transfer() {
/>
) : null}
</div>
{isCheckingWrapped ? null : (
{isCheckingWrapped ? null : canAttemptAttest ? (
<Typography variant="body2">
<br />
This token does not exist on {CHAINS_BY_ID[toChain].name}. Someone
must attest the the token to the target chain before it can be
transferred.
</Typography>
) : (
<Typography variant="body2" color="error">
{!isAttestImplemented
? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}`
: !isProviderConnected
? "The source wallet is not connected"
: !isRecipientAvailable
? "The receiving wallet is not connected"
: !isAddressDefined
? "Please provide an asset address"
: ""}
</Typography>
)}
</>
)}

View File

@ -1,10 +1,13 @@
import { ethers } from "ethers";
import { useEffect, useState } from "react";
import { ChainId, CHAIN_ID_ETH } from "../utils/consts";
import { wrappedAssetEth } from "../utils/wrappedAsset";
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "../utils/consts";
import {
getAttestedAssetEth,
getAttestedAssetSol,
} from "../utils/getAttestedAsset";
export interface WrappedAssetState {
isLoading: boolean;
isWrapped: boolean;
wrappedAsset: string | null;
}
@ -16,19 +19,38 @@ function useWrappedAsset(
) {
const [state, setState] = useState<WrappedAssetState>({
isLoading: false,
isWrapped: false,
wrappedAsset: null,
});
useEffect(() => {
let cancelled = false;
(async () => {
if (provider && checkChain === CHAIN_ID_ETH) {
setState({ isLoading: true, wrappedAsset: null });
const asset = await wrappedAssetEth(provider, originChain, originAsset);
if (checkChain === CHAIN_ID_ETH && provider) {
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
const asset = await getAttestedAssetEth(
provider,
originChain,
originAsset
);
if (!cancelled) {
setState({ isLoading: false, wrappedAsset: asset });
setState({
isLoading: false,
isWrapped: !!asset && asset !== ethers.constants.AddressZero,
wrappedAsset: asset,
});
}
} else if (checkChain === CHAIN_ID_SOLANA) {
setState({ isLoading: true, isWrapped: false, wrappedAsset: null });
const asset = await getAttestedAssetSol(originChain, originAsset);
if (!cancelled) {
setState({
isLoading: false,
isWrapped: !!asset,
wrappedAsset: asset,
});
}
} else {
setState({ isLoading: false, wrappedAsset: null });
setState({ isLoading: false, isWrapped: false, wrappedAsset: null });
}
})();
return () => {

View File

@ -1,9 +1,33 @@
import {
AccountMeta,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import {
GrpcWebImpl,
PublicrpcClientImpl,
} from "../proto/publicrpc/v1/publicrpc";
import { ChainId } from "../utils/consts";
// begin from clients\solana\main.ts
export function ixFromRust(data: any): TransactionInstruction {
let keys: Array<AccountMeta> = data.accounts.map(accountMetaFromRust);
return new TransactionInstruction({
programId: new PublicKey(data.program_id),
data: Buffer.from(data.data),
keys: keys,
});
}
function accountMetaFromRust(meta: any): AccountMeta {
return {
pubkey: new PublicKey(meta.pubkey),
isSigner: meta.is_signer,
isWritable: meta.is_writable,
};
}
// end from clients\solana\main.ts
export async function getSignedVAA(
emitterChain: ChainId,
emitterAddress: string,

View File

@ -0,0 +1,162 @@
import Wallet from "@project-serum/sol-wallet-adapter";
import {
Connection,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { ethers } from "ethers";
import { arrayify, zeroPad } from "ethers/lib/utils";
import { Bridge__factory, Implementation__factory } from "../ethers-contracts";
import { getSignedVAA, ixFromRust } from "../sdk";
import {
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
ETH_BRIDGE_ADDRESS,
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "./consts";
// TODO: this should probably be extended from the context somehow so that the signatures match
// TODO: allow for / handle cancellation?
// TODO: overall better input checking and error handling
export function attestFromEth(
provider: ethers.providers.Web3Provider | undefined,
tokenAddress: string
) {
if (!provider) return;
const signer = provider.getSigner();
if (!signer) return;
//TODO: more catches
(async () => {
const signerAddress = await signer.getAddress();
console.log("Signer:", signerAddress);
console.log("Token:", tokenAddress);
const nonceConst = Math.random() * 100000;
const nonceBuffer = Buffer.alloc(4);
nonceBuffer.writeUInt32LE(nonceConst, 0);
console.log("Initiating attestation");
console.log("Nonce:", nonceBuffer);
const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer);
const v = await bridge.attestToken(tokenAddress, nonceBuffer);
const receipt = await v.wait();
// TODO: dangerous!(?)
const bridgeLog = receipt.logs.filter((l) => {
console.log(l.address, ETH_BRIDGE_ADDRESS);
return l.address === ETH_BRIDGE_ADDRESS;
})[0];
const {
args: { sequence },
} = Implementation__factory.createInterface().parseLog(bridgeLog);
console.log("SEQ:", sequence);
const emitterAddress = Buffer.from(
zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32)
).toString("hex");
const { vaaBytes } = await getSignedVAA(
CHAIN_ID_ETH,
emitterAddress,
sequence
);
console.log("SIGNED VAA:", vaaBytes);
})();
}
// TODO: need to check transfer native vs transfer wrapped
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
export function attestFromSolana(
wallet: Wallet | undefined,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
mintAddress: string,
decimals: number
) {
if (!wallet || !wallet.publicKey || !payerAddress) return;
(async () => {
const nonceConst = Math.random() * 100000;
const nonceBuffer = Buffer.alloc(4);
nonceBuffer.writeUInt32LE(nonceConst, 0);
const nonce = nonceBuffer.readUInt32LE(0);
console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS);
console.log("bridge:", SOL_BRIDGE_ADDRESS);
console.log("payer:", payerAddress);
console.log("token:", mintAddress);
console.log("nonce:", nonce);
const bridge = await import("bridge");
const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS);
const bridgeStatePK = new PublicKey(
bridge.state_address(SOL_BRIDGE_ADDRESS)
);
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const bridgeStateAccountInfo = await connection.getAccountInfo(
bridgeStatePK
);
if (bridgeStateAccountInfo?.data === undefined) {
throw new Error("bridge state not found");
}
const bridgeState = bridge.parse_state(
new Uint8Array(bridgeStateAccountInfo?.data)
);
const transferIx = SystemProgram.transfer({
fromPubkey: new PublicKey(payerAddress),
toPubkey: new PublicKey(feeAccount),
lamports: bridgeState.config.fee,
});
// TODO: pass in connection
// Add transfer instruction to transaction
const { attest_ix, emitter_address } = await import("token-bridge");
const ix = ixFromRust(
attest_ix(
SOL_TOKEN_BRIDGE_ADDRESS,
SOL_BRIDGE_ADDRESS,
payerAddress,
mintAddress,
decimals,
mintAddress, // TODO: automate on wasm side
nonce
)
);
const transaction = new Transaction().add(transferIx, ix);
const { blockhash } = await connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(payerAddress);
// Sign transaction, broadcast, and confirm
const signed = await wallet.signTransaction(transaction);
console.log("SIGNED", signed);
const txid = await connection.sendRawTransaction(signed.serialize());
console.log("SENT", txid);
const conf = await connection.confirmTransaction(txid);
console.log("CONFIRMED", conf);
const info = await connection.getTransaction(txid);
console.log("INFO", info);
// TODO: better parsing, safer
const SEQ_LOG = "Program log: Sequence: ";
const sequence = info?.meta?.logMessages
?.filter((msg) => msg.startsWith(SEQ_LOG))[0]
.replace(SEQ_LOG, "");
if (!sequence) {
throw new Error("sequence not found");
}
console.log("SEQ", sequence);
const emitterAddress = Buffer.from(
zeroPad(
new PublicKey(emitter_address(SOL_TOKEN_BRIDGE_ADDRESS)).toBytes(),
32
)
).toString("hex");
const { vaaBytes } = await getSignedVAA(
CHAIN_ID_SOLANA,
emitterAddress,
sequence
);
console.log("SIGNED VAA:", vaaBytes);
})();
}
const attestFrom = {
[CHAIN_ID_ETH]: attestFromEth,
[CHAIN_ID_SOLANA]: attestFromSolana,
};
export default attestFrom;

View File

@ -0,0 +1,56 @@
import { Connection, PublicKey } from "@solana/web3.js";
import { ethers } from "ethers";
import { arrayify, isHexString, zeroPad } from "ethers/lib/utils";
import { Bridge__factory } from "../ethers-contracts";
import {
ChainId,
CHAIN_ID_SOLANA,
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST,
SOL_TOKEN_BRIDGE_ADDRESS,
} from "./consts";
export async function getAttestedAssetEth(
provider: ethers.providers.Web3Provider,
originChain: ChainId,
originAsset: string
) {
const tokenBridge = Bridge__factory.connect(
ETH_TOKEN_BRIDGE_ADDRESS,
provider
);
// TODO: address conversion may be more complex than this
const originAssetBytes = zeroPad(
originChain === CHAIN_ID_SOLANA
? new PublicKey(originAsset).toBytes()
: arrayify(originAsset),
32
);
return await tokenBridge.wrappedAsset(originChain, originAssetBytes);
}
export async function getAttestedAssetSol(
originChain: ChainId,
originAsset: string
) {
if (!isHexString(originAsset)) return null;
const { wrapped_address } = await import("token-bridge");
// TODO: address conversion may be more complex than this
const originAssetBytes = zeroPad(
arrayify(originAsset, { hexPad: "left" }),
32
);
const wrappedAddress = wrapped_address(
SOL_TOKEN_BRIDGE_ADDRESS,
originAssetBytes,
originChain
);
const wrappedAddressPK = new PublicKey(wrappedAddress);
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const wrappedAssetAccountInfo = await connection.getAccountInfo(
wrappedAddressPK
);
console.log("WAAI", wrappedAssetAccountInfo);
return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
}

View File

@ -1,12 +1,10 @@
import Wallet from "@project-serum/sol-wallet-adapter";
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
AccountMeta,
Connection,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import { ethers } from "ethers";
import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils";
@ -15,7 +13,7 @@ import {
Implementation__factory,
TokenImplementation__factory,
} from "../ethers-contracts";
import { getSignedVAA } from "../sdk";
import { getSignedVAA, ixFromRust } from "../sdk";
import {
ChainId,
CHAIN_ID_ETH,
@ -32,6 +30,7 @@ import {
// TODO: overall better input checking and error handling
export function transferFromEth(
provider: ethers.providers.Web3Provider | undefined,
// TODO: specify signer
tokenAddress: string,
amount: string,
recipientChain: ChainId,
@ -100,26 +99,6 @@ export function transferFromEth(
})();
}
// TODO: should we share this with client? ooh, should client use the SDK ;)
// begin from clients\solana\main.ts
function ixFromRust(data: any): TransactionInstruction {
let keys: Array<AccountMeta> = data.accounts.map(accountMetaFromRust);
return new TransactionInstruction({
programId: new PublicKey(data.program_id),
data: Buffer.from(data.data),
keys: keys,
});
}
function accountMetaFromRust(meta: any): AccountMeta {
return {
pubkey: new PublicKey(meta.pubkey),
isSigner: meta.is_signer,
isWritable: meta.is_writable,
};
}
// end from clients\solana\main.ts
// TODO: need to check transfer native vs transfer wrapped
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
export function transferFromSolana(
@ -166,6 +145,7 @@ export function transferFromSolana(
const bridgeStatePK = new PublicKey(
bridge.state_address(SOL_BRIDGE_ADDRESS)
);
// TODO: share connection in context?
const connection = new Connection(SOLANA_HOST, "confirmed");
const bridgeStateAccountInfo = await connection.getAccountInfo(
bridgeStatePK

View File

@ -1,24 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { ethers } from "ethers";
import { arrayify, zeroPad } from "ethers/lib/utils";
import { Bridge__factory } from "../ethers-contracts";
import { ChainId, CHAIN_ID_SOLANA, ETH_TOKEN_BRIDGE_ADDRESS } from "./consts";
export function wrappedAssetEth(
provider: ethers.providers.Web3Provider,
originChain: ChainId,
originAsset: string
) {
const tokenBridge = Bridge__factory.connect(
ETH_TOKEN_BRIDGE_ADDRESS,
provider
);
// TODO: address conversion may be more complex than this
const originAssetBytes = zeroPad(
originChain === CHAIN_ID_SOLANA
? new PublicKey(originAsset).toBytes()
: arrayify(originAsset),
32
);
return tokenBridge.wrappedAsset(originChain, originAssetBytes);
}