bridge_ui: one step closer to solana transfers

Change-Id: Ief6a8b73458cbfbc7b8d5655ddc6c430a65b2b8f
This commit is contained in:
Evan Gray 2021-08-02 15:23:37 -04:00 committed by Hendrik Hofstadt
parent b1a237db99
commit 3aecf65f4d
7 changed files with 314 additions and 69 deletions

View File

@ -12,6 +12,7 @@
"@material-ui/core": "^4.12.2", "@material-ui/core": "^4.12.2",
"@metamask/detect-provider": "^1.2.0", "@metamask/detect-provider": "^1.2.0",
"@project-serum/sol-wallet-adapter": "^0.2.5", "@project-serum/sol-wallet-adapter": "^0.2.5",
"@solana/spl-token": "^0.1.6",
"@solana/wallet-base": "^0.0.1", "@solana/wallet-base": "^0.0.1",
"@solana/web3.js": "^1.22.0", "@solana/web3.js": "^1.22.0",
"@typechain/ethers-v5": "^7.0.1", "@typechain/ethers-v5": "^7.0.1",
@ -5367,6 +5368,53 @@
"ieee754": "^1.2.1" "ieee754": "^1.2.1"
} }
}, },
"node_modules/@solana/spl-token": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.6.tgz",
"integrity": "sha512-fYj+a3w1bqWN6Ibf85XF3h2JkuxevI3Spvqi+mjsNqVUEo2AgxxTZmujNLn/jIzQDNdWkBfF/wYzH5ikcGHmfw==",
"dependencies": {
"@babel/runtime": "^7.10.5",
"@solana/web3.js": "^1.12.0",
"bn.js": "^5.1.0",
"buffer": "6.0.3",
"buffer-layout": "^1.2.0",
"dotenv": "10.0.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/@solana/spl-token/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/@solana/spl-token/node_modules/dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"engines": {
"node": ">=10"
}
},
"node_modules/@solana/wallet-base": { "node_modules/@solana/wallet-base": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/@solana/wallet-base/-/wallet-base-0.0.1.tgz", "resolved": "https://registry.npmjs.org/@solana/wallet-base/-/wallet-base-0.0.1.tgz",
@ -42465,6 +42513,35 @@
} }
} }
}, },
"@solana/spl-token": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.6.tgz",
"integrity": "sha512-fYj+a3w1bqWN6Ibf85XF3h2JkuxevI3Spvqi+mjsNqVUEo2AgxxTZmujNLn/jIzQDNdWkBfF/wYzH5ikcGHmfw==",
"requires": {
"@babel/runtime": "^7.10.5",
"@solana/web3.js": "^1.12.0",
"bn.js": "^5.1.0",
"buffer": "6.0.3",
"buffer-layout": "^1.2.0",
"dotenv": "10.0.0"
},
"dependencies": {
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
}
}
},
"@solana/wallet-base": { "@solana/wallet-base": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/@solana/wallet-base/-/wallet-base-0.0.1.tgz", "resolved": "https://registry.npmjs.org/@solana/wallet-base/-/wallet-base-0.0.1.tgz",

View File

@ -6,6 +6,7 @@
"@material-ui/core": "^4.12.2", "@material-ui/core": "^4.12.2",
"@metamask/detect-provider": "^1.2.0", "@metamask/detect-provider": "^1.2.0",
"@project-serum/sol-wallet-adapter": "^0.2.5", "@project-serum/sol-wallet-adapter": "^0.2.5",
"@solana/spl-token": "^0.1.6",
"@solana/wallet-base": "^0.0.1", "@solana/wallet-base": "^0.0.1",
"@solana/web3.js": "^1.22.0", "@solana/web3.js": "^1.22.0",
"@typechain/ethers-v5": "^7.0.1", "@typechain/ethers-v5": "^7.0.1",

View File

@ -23,7 +23,7 @@ function KeyAndBalance({
); );
const { wallet: solWallet } = useSolanaWallet(); const { wallet: solWallet } = useSolanaWallet();
const solPK = solWallet?.publicKey; const solPK = solWallet?.publicKey;
const solBalance = useSolanaBalance( const { uiAmountString: solBalance } = useSolanaBalance(
tokenAddress, tokenAddress,
solPK, solPK,
chainId === CHAIN_ID_SOLANA chainId === CHAIN_ID_SOLANA

View File

@ -100,6 +100,11 @@ function Transfer() {
const provider = useEthereumProvider(); const provider = useEthereumProvider();
const { wallet } = useSolanaWallet(); const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey; const solPK = wallet?.publicKey;
const {
tokenAccount: solTokenPK,
decimals: solDecimals,
uiAmount: solBalance,
} = useSolanaBalance(assetAddress, solPK, fromChain === CHAIN_ID_SOLANA);
// TODO: dynamically get "to" wallet // TODO: dynamically get "to" wallet
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
// TODO: more generic way of calling these // TODO: more generic way of calling these
@ -121,27 +126,35 @@ function Transfer() {
transferFrom[fromChain] === transferFromSolana transferFrom[fromChain] === transferFromSolana
) { ) {
transferFromSolana( transferFromSolana(
wallet,
solPK?.toString(), solPK?.toString(),
solTokenPK?.toString(),
assetAddress, assetAddress,
amount, amount,
solDecimals,
provider, provider,
toChain toChain
); );
} }
} }
}, [fromChain, provider, solPK, assetAddress, amount, toChain]); }, [
fromChain,
provider,
wallet,
solPK,
solTokenPK,
assetAddress,
amount,
solDecimals,
toChain,
]);
// update this as we develop, just setting expectations with the button state // update this as we develop, just setting expectations with the button state
const ethBalance = useEthereumBalance( const ethBalance = useEthereumBalance(
assetAddress, assetAddress,
provider, provider,
fromChain === CHAIN_ID_ETH fromChain === CHAIN_ID_ETH
); );
const solBalance = useSolanaBalance( const balance = Number(ethBalance) || solBalance;
assetAddress,
solPK,
fromChain === CHAIN_ID_SOLANA
);
const balance = Number(ethBalance) || Number(solBalance);
const isTransferImplemented = !!transferFrom[fromChain]; const isTransferImplemented = !!transferFrom[fromChain];
const isProviderConnected = !!provider; const isProviderConnected = !!provider;
const isRecipientAvailable = !!solPK; const isRecipientAvailable = !!solPK;

View File

@ -2,20 +2,46 @@ import { Connection, PublicKey } from "@solana/web3.js";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SOLANA_HOST } from "../utils/consts"; import { SOLANA_HOST } from "../utils/consts";
export interface Balance {
tokenAccount: PublicKey | undefined;
amount: string;
decimals: number;
uiAmount: number;
uiAmountString: string;
}
function createBalance(
tokenAccount: PublicKey | undefined,
amount: string,
decimals: number,
uiAmount: number,
uiAmountString: string
) {
return {
tokenAccount,
amount,
decimals,
uiAmount,
uiAmountString,
};
}
function useSolanaBalance( function useSolanaBalance(
tokenAddress: string | undefined, tokenAddress: string | undefined,
ownerAddress: PublicKey | null | undefined, ownerAddress: PublicKey | null | undefined,
shouldCalculate?: boolean shouldCalculate?: boolean
) { ) {
//TODO: should connection happen in a context? //TODO: should connection happen in a context?
const [balance, setBalance] = useState<string>(""); const [balance, setBalance] = useState<Balance>(
createBalance(undefined, "", 0, 0, "")
);
useEffect(() => { useEffect(() => {
if (!tokenAddress || !ownerAddress || !shouldCalculate) { if (!tokenAddress || !ownerAddress || !shouldCalculate) {
setBalance(""); setBalance(createBalance(undefined, "", 0, 0, ""));
return; return;
} }
let cancelled = false; let cancelled = false;
const connection = new Connection(SOLANA_HOST); const connection = new Connection(SOLANA_HOST, "finalized");
connection connection
.getParsedTokenAccountsByOwner(ownerAddress, { .getParsedTokenAccountsByOwner(ownerAddress, {
mint: new PublicKey(tokenAddress), mint: new PublicKey(tokenAddress),
@ -23,18 +49,23 @@ function useSolanaBalance(
.then(({ value }) => { .then(({ value }) => {
if (!cancelled) { if (!cancelled) {
if (value.length) { if (value.length) {
console.log(value[0].account.data.parsed);
setBalance( setBalance(
value[0].account.data.parsed?.info?.tokenAmount?.uiAmountString createBalance(
value[0].pubkey,
value[0].account.data.parsed?.info?.tokenAmount?.amount,
value[0].account.data.parsed?.info?.tokenAmount?.decimals,
value[0].account.data.parsed?.info?.tokenAmount?.uiAmount,
value[0].account.data.parsed?.info?.tokenAmount?.uiAmountString
)
); );
} else { } else {
setBalance("0"); setBalance(createBalance(undefined, "0", 0, 0, "0"));
} }
} }
}) })
.catch(() => { .catch(() => {
if (!cancelled) { if (!cancelled) {
setBalance(""); setBalance(createBalance(undefined, "", 0, 0, ""));
} }
}); });
return () => { return () => {

View File

@ -1,34 +1,42 @@
export type ChainId = 1 | 2 | 3 | 4 export type ChainId = 1 | 2 | 3 | 4;
export const CHAIN_ID_SOLANA: ChainId = 1 export const CHAIN_ID_SOLANA: ChainId = 1;
export const CHAIN_ID_ETH: ChainId = 2 export const CHAIN_ID_ETH: ChainId = 2;
export const CHAIN_ID_TERRA: ChainId = 3 export const CHAIN_ID_TERRA: ChainId = 3;
export const CHAIN_ID_BSC: ChainId = 4 export const CHAIN_ID_BSC: ChainId = 4;
export interface ChainInfo { export interface ChainInfo {
id: ChainId id: ChainId;
name: string name: string;
} }
export const CHAINS = [ export const CHAINS = [
{ {
id: CHAIN_ID_BSC, id: CHAIN_ID_BSC,
name: 'Binance Smart Chain' name: "Binance Smart Chain",
}, },
{ {
id: CHAIN_ID_ETH, id: CHAIN_ID_ETH,
name: 'Ethereum' name: "Ethereum",
}, },
{ {
id: CHAIN_ID_SOLANA, id: CHAIN_ID_SOLANA,
name: 'Solana' name: "Solana",
}, },
{ {
id: CHAIN_ID_TERRA, id: CHAIN_ID_TERRA,
name: 'Terra' name: "Terra",
}, },
] ];
export type ChainsById = {[key in ChainId]: ChainInfo} export type ChainsById = { [key in ChainId]: ChainInfo };
export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain)=>{obj[chain.id]=chain;return obj},{} as ChainsById) export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => {
export const SOLANA_HOST = 'http://localhost:8899' obj[chain.id] = chain;
export const ETH_TEST_TOKEN_ADDRESS = "0x0290FB167208Af455bB137780163b7B7a9a10C16" return obj;
export const ETH_TOKEN_BRIDGE_ADDRESS = "0xe982e462b094850f12af94d21d470e21be9d0e9c" }, {} as ChainsById);
export const SOL_TEST_TOKEN_ADDRESS = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ" export const SOLANA_HOST = "http://localhost:8899";
export const SOL_TOKEN_BRIDGE_ADDRESS = "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE" export const ETH_TEST_TOKEN_ADDRESS =
"0x0290FB167208Af455bB137780163b7B7a9a10C16";
export const ETH_TOKEN_BRIDGE_ADDRESS =
"0xe982e462b094850f12af94d21d470e21be9d0e9c";
export const SOL_TEST_TOKEN_ADDRESS =
"2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ";
export const SOL_BRIDGE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
export const SOL_TOKEN_BRIDGE_ADDRESS =
"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";

View File

@ -1,12 +1,39 @@
import Wallet from "@project-serum/sol-wallet-adapter";
import {
AccountMeta,
Connection,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { ethers } from "ethers"; import { ethers } from "ethers";
import { arrayify, formatUnits, parseUnits } from "ethers/lib/utils"; import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils";
import { Bridge__factory, TokenImplementation__factory } from "../ethers-contracts"; import {
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA, ETH_TOKEN_BRIDGE_ADDRESS, SOL_TOKEN_BRIDGE_ADDRESS } from "./consts"; Bridge__factory,
TokenImplementation__factory,
} from "../ethers-contracts";
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
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: this should probably be extended from the context somehow so that the signatures match
// TODO: allow for / handle cancellation? // TODO: allow for / handle cancellation?
// TODO: overall better input checking and error handling // TODO: overall better input checking and error handling
export function transferFromEth(provider: ethers.providers.Web3Provider | undefined, tokenAddress: string, amount: string, recipientChain: ChainId, recipientAddress: Uint8Array | undefined) { export function transferFromEth(
provider: ethers.providers.Web3Provider | undefined,
tokenAddress: string,
amount: string,
recipientChain: ChainId,
recipientAddress: Uint8Array | undefined
) {
if (!provider || !recipientAddress) return; if (!provider || !recipientAddress) return;
const signer = provider.getSigner(); const signer = provider.getSigner();
if (!signer) return; if (!signer) return;
@ -16,11 +43,8 @@ export function transferFromEth(provider: ethers.providers.Web3Provider | undefi
const amountParsed = parseUnits(amount, 18); const amountParsed = parseUnits(amount, 18);
signer.getAddress().then((signerAddress) => { signer.getAddress().then((signerAddress) => {
console.log("Signer:", signerAddress); console.log("Signer:", signerAddress);
console.log("Token:", tokenAddress) console.log("Token:", tokenAddress);
const token = TokenImplementation__factory.connect( const token = TokenImplementation__factory.connect(tokenAddress, signer);
tokenAddress,
signer
);
token token
.allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS) .allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS)
.then((allowance) => { .then((allowance) => {
@ -59,41 +83,132 @@ export function transferFromEth(provider: ethers.providers.Web3Provider | undefi
}); });
} }
// 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: need to check transfer native vs transfer wrapped
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts) // TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
export function transferFromSolana(fromAddress: string | undefined, tokenAddress: string, amount: string, targetProvider: ethers.providers.Web3Provider | undefined, targetChain: ChainId) { export function transferFromSolana(
if (!fromAddress || !targetProvider) return; wallet: Wallet | undefined,
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
fromAddress: string | undefined,
mintAddress: string,
amount: string,
decimals: number,
targetProvider: ethers.providers.Web3Provider | undefined,
targetChain: ChainId
) {
if (
!wallet ||
!wallet.publicKey ||
!payerAddress ||
!fromAddress ||
!targetProvider
)
return;
const targetSigner = targetProvider.getSigner(); const targetSigner = targetProvider.getSigner();
if (!targetSigner) return; if (!targetSigner) return;
targetSigner.getAddress().then(targetAddressStr => { (async () => {
const targetAddress = arrayify(targetAddressStr) const targetAddressStr = await targetSigner.getAddress();
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
const nonceConst = Math.random() * 100000; const nonceConst = Math.random() * 100000;
const nonceBuffer = Buffer.alloc(4); const nonceBuffer = Buffer.alloc(4);
nonceBuffer.writeUInt32LE(nonceConst, 0); nonceBuffer.writeUInt32LE(nonceConst, 0);
const nonce = nonceBuffer.readUInt32LE(0) const nonce = nonceBuffer.readUInt32LE(0);
// TODO: check decimals const amountParsed = parseUnits(amount, decimals).toBigInt();
// should we avoid BigInt? const fee = BigInt(0); // for now, this won't do anything, we may add later
const amountParsed = BigInt(amount) console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS);
const fee = BigInt(0) // for now, this won't do anything, we may add later console.log("bridge:", SOL_BRIDGE_ADDRESS);
console.log('bridge:',SOL_TOKEN_BRIDGE_ADDRESS) console.log("payer:", payerAddress);
console.log('from:',fromAddress) console.log("from:", fromAddress);
console.log('token:',tokenAddress) console.log("token:", mintAddress);
console.log('nonce:',nonce) console.log("nonce:", nonce);
console.log('amount:',amountParsed) console.log("amount:", amountParsed);
console.log('fee:',fee) console.log("fee:", fee);
console.log('target:',targetAddressStr,targetAddress) console.log("target:", targetAddressStr, targetAddress);
console.log('chain:',targetChain) console.log("chain:", targetChain);
// TODO: program_id vs bridge_id? const bridge = await import("bridge");
import("token-bridge").then(({transfer_native_ix})=>{ const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS);
const ix = transfer_native_ix(SOL_TOKEN_BRIDGE_ADDRESS,SOL_TOKEN_BRIDGE_ADDRESS,fromAddress,fromAddress,tokenAddress,nonce,amountParsed,fee,targetAddress,targetChain) const bridgeStatePK = new PublicKey(
console.log(ix) bridge.state_address(SOL_BRIDGE_ADDRESS)
}) );
}) 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 { transfer_native_ix, approval_authority_address } = await import(
"token-bridge"
);
const approvalIx = Token.createApproveInstruction(
TOKEN_PROGRAM_ID,
new PublicKey(fromAddress),
new PublicKey(approval_authority_address(SOL_TOKEN_BRIDGE_ADDRESS)),
new PublicKey(payerAddress),
[],
Number(amountParsed)
);
const ix = ixFromRust(
transfer_native_ix(
SOL_TOKEN_BRIDGE_ADDRESS,
SOL_BRIDGE_ADDRESS,
payerAddress,
fromAddress,
mintAddress,
nonce,
amountParsed,
fee,
targetAddress,
targetChain
)
);
console.log(ix);
const transaction = new Transaction().add(transferIx, approvalIx, 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);
await connection.confirmTransaction(txid);
console.log("CONFIRMED");
})();
} }
const transferFrom = { const transferFrom = {
[CHAIN_ID_ETH]: transferFromEth, [CHAIN_ID_ETH]: transferFromEth,
[CHAIN_ID_SOLANA]: transferFromSolana [CHAIN_ID_SOLANA]: transferFromSolana,
} };
export default transferFrom export default transferFrom;