bridge_ui: attestFrom eth and sol
Change-Id: I2eed25b47bcac8891e059d0e11aa624aba802c47
This commit is contained in:
parent
8444363ac8
commit
012c30b30b
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue