Implement assistant & Reimplement wrapped asset precreation (#42)

* all: readd early wrapped meta creation; initial transfer wizard

* web: complete transfer assistant

* web: allow multiple accounts per wrapped mint
This commit is contained in:
Hendrik Hofstadt 2020-10-14 11:49:13 +02:00 committed by GitHub
parent a0b3b1bf0c
commit e266bf1a7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2028 additions and 16223 deletions

View File

@ -167,6 +167,9 @@ uint256 amount
#### Transfer of assets Foreign Chain -> Root Chain #### Transfer of assets Foreign Chain -> Root Chain
If this is the first time the asset is transferred to the root chain, the user inititates a `CreateWrapped` instruction
on the root chain to initialize the wrapped asset.
The user creates a token account for the wrapped asset on the root chain. The user creates a token account for the wrapped asset on the root chain.
The user sends a chain native asset to the bridge on the foreign chain using the `Lock` function. The user sends a chain native asset to the bridge on the foreign chain using the `Lock` function.

View File

@ -24,6 +24,20 @@ Pokes a `TransferOutProposal` so it is reprocessed by the guardians.
| ----- | ------ | ------------ | ------ | --------- | ----- | ------- | | ----- | ------ | ------------ | ------ | --------- | ----- | ------- |
| 0 | proposal | TransferOutProposal | | ✅ | | ✅ | | 0 | proposal | TransferOutProposal | | ✅ | | ✅ |
#### CreateWrappedAsset
Creates a new `WrappedAsset` to be used to create accounts and later receive transfers on chain.
| Index | Name | Type | signer | writeable | empty | derived |
| ----- | -------- | ------------------- | ------ | --------- | ----- | ------- |
| 0 | sys | SystemProgram | | | | |
| 1 | token_program | SplToken | | | | |
| 2 | rent | Sysvar | | | | ✅ |
| 3 | bridge | BridgeConfig | | | | |
| 4 | payer | Account | ✅ | | | |
| 5 | wrapped_mint | WrappedAsset | | | ✅ | ✅ |
| 6 | wrapped_meta_account | WrappedAssetMeta | | ✅ | ✅ | ✅ |
#### VerifySignatures #### VerifySignatures
Checks secp checks (in the previous instruction) and stores results. Checks secp checks (in the previous instruction) and stores results.

View File

@ -12,7 +12,7 @@ use solana_sdk::{
use crate::{ use crate::{
instruction::BridgeInstruction::{ instruction::BridgeInstruction::{
Initialize, PokeProposal, PostVAA, TransferOut, VerifySignatures, CreateWrapped, Initialize, PokeProposal, PostVAA, TransferOut, VerifySignatures,
}, },
state::{AssetMeta, Bridge, BridgeConfig}, state::{AssetMeta, Bridge, BridgeConfig},
vaa::{VAABody, VAA}, vaa::{VAABody, VAA},
@ -139,6 +139,9 @@ pub enum BridgeInstruction {
/// Verifies signature instructions /// Verifies signature instructions
VerifySignatures(VerifySigPayload), VerifySignatures(VerifySigPayload),
/// Creates a new wrapped asset
CreateWrapped(AssetMeta),
} }
impl BridgeInstruction { impl BridgeInstruction {
@ -175,6 +178,11 @@ impl BridgeInstruction {
VerifySignatures(*payload) VerifySignatures(*payload)
} }
7 => {
let payload: &AssetMeta = unpack(input)?;
CreateWrapped(*payload)
}
_ => return Err(ProgramError::InvalidInstructionData), _ => return Err(ProgramError::InvalidInstructionData),
}) })
} }
@ -239,6 +247,14 @@ impl BridgeInstruction {
}; };
*value = payload; *value = payload;
} }
Self::CreateWrapped(payload) => {
output.resize(size_of::<AssetMeta>() + 1, 0);
output[0] = 7;
#[allow(clippy::cast_ptr_alignment)]
let value =
unsafe { &mut *(&mut output[size_of::<u8>()] as *mut u8 as *mut AssetMeta) };
*value = payload;
}
} }
Ok(output) Ok(output)
} }
@ -435,6 +451,7 @@ pub fn post_vaa(
program_id, program_id,
&bridge_key, &bridge_key,
t.asset.chain, t.asset.chain,
t.asset.decimals,
t.asset.address, t.asset.address,
)?; )?;
let wrapped_meta_key = let wrapped_meta_key =
@ -454,6 +471,41 @@ pub fn post_vaa(
}) })
} }
/// Creates a 'CreateWrapped' instruction.
pub fn create_wrapped(
program_id: &Pubkey,
payer: &Pubkey,
meta: AssetMeta,
) -> Result<Instruction, ProgramError> {
let data = BridgeInstruction::CreateWrapped(meta).serialize()?;
let bridge_key = Bridge::derive_bridge_id(program_id)?;
let wrapped_mint_key = Bridge::derive_wrapped_asset_id(
program_id,
&bridge_key,
meta.chain,
meta.decimals,
meta.address,
)?;
let wrapped_meta_key =
Bridge::derive_wrapped_meta_id(program_id, &bridge_key, &wrapped_mint_key)?;
let accounts = vec![
AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new(bridge_key, false),
AccountMeta::new(*payer, true),
AccountMeta::new(wrapped_mint_key, false),
AccountMeta::new(wrapped_meta_key, false),
];
Ok(Instruction {
program_id: *program_id,
accounts,
data,
})
}
/// Creates an 'PokeProposal' instruction. /// Creates an 'PokeProposal' instruction.
#[cfg(not(target_arch = "bpf"))] #[cfg(not(target_arch = "bpf"))]
pub fn poke_proposal( pub fn poke_proposal(

View File

@ -90,6 +90,10 @@ impl Bridge {
Self::process_verify_signatures(program_id, accounts, &p) Self::process_verify_signatures(program_id, accounts, &p)
} }
CreateWrapped(meta) => {
info!("Instruction: CreateWrapped");
Self::process_create_wrapped(program_id, accounts, &meta)
}
_ => panic!(""), _ => panic!(""),
} }
} }
@ -385,6 +389,7 @@ impl Bridge {
program_id, program_id,
bridge_info.key, bridge_info.key,
t.asset.chain, t.asset.chain,
t.asset.decimals,
t.asset.address, t.asset.address,
)?; )?;
if expected_mint_address != *mint_info.key { if expected_mint_address != *mint_info.key {
@ -657,7 +662,6 @@ impl Bridge {
accounts, accounts,
account_info_iter, account_info_iter,
bridge_info, bridge_info,
payer_info,
&mut bridge, &mut bridge,
&v, &v,
) )
@ -755,7 +759,6 @@ impl Bridge {
accounts: &[AccountInfo], accounts: &[AccountInfo],
account_info_iter: &mut Iter<AccountInfo>, account_info_iter: &mut Iter<AccountInfo>,
bridge_info: &AccountInfo, bridge_info: &AccountInfo,
payer_info: &AccountInfo,
bridge: &mut Bridge, bridge: &mut Bridge,
b: &BodyTransfer, b: &BodyTransfer,
) -> ProgramResult { ) -> ProgramResult {
@ -786,52 +789,16 @@ impl Bridge {
b.amount, b.amount,
)?; )?;
} else { } else {
// Create wrapped asset if it does not exist // Foreign chain asset, mint wrapped asset
if mint_info.data_is_empty() { let expected_mint_address = Bridge::derive_wrapped_asset_id(
let meta_info = next_account_info(account_info_iter)?; program_id,
bridge_info.key,
// Foreign chain asset, mint wrapped asset b.asset.chain,
let expected_mint_address = Bridge::derive_wrapped_asset_id( b.asset.decimals,
program_id, b.asset.address,
bridge_info.key, )?;
b.asset.chain, if expected_mint_address != *mint_info.key {
b.asset.address, return Err(Error::InvalidDerivedAccount.into());
)?;
if expected_mint_address != *mint_info.key {
return Err(Error::InvalidDerivedAccount.into());
}
// Create wrapped mint
Self::create_wrapped_mint(
program_id,
accounts,
&bridge.config.token_program,
mint_info.key,
bridge_info.key,
payer_info.key,
&b.asset,
b.asset.decimals,
)?;
// Check and create wrapped asset meta to allow reverse resolution of info
let wrapped_meta_seeds =
Bridge::derive_wrapped_meta_seeds(bridge_info.key, mint_info.key);
Bridge::check_and_create_account::<WrappedAssetMeta>(
program_id,
accounts,
meta_info.key,
payer_info.key,
program_id,
&wrapped_meta_seeds,
)?;
let mut wrapped_meta_data = meta_info.data.borrow_mut();
let wrapped_meta: &mut WrappedAssetMeta =
Bridge::unpack_unchecked(&mut wrapped_meta_data)?;
wrapped_meta.is_initialized = true;
wrapped_meta.address = b.asset.address;
wrapped_meta.chain = b.asset.chain;
} }
// This automatically asserts that the mint was created by this account by using // This automatically asserts that the mint was created by this account by using
@ -900,6 +867,69 @@ impl Bridge {
Ok(()) Ok(())
} }
/// Creates a new wrapped asset
pub fn process_create_wrapped(
program_id: &Pubkey,
accounts: &[AccountInfo],
a: &AssetMeta,
) -> ProgramResult {
info!("create wrapped");
let account_info_iter = &mut accounts.iter();
next_account_info(account_info_iter)?; // System program
next_account_info(account_info_iter)?; // Token program
next_account_info(account_info_iter)?; // Rent sysvar
let bridge_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let mint_info = next_account_info(account_info_iter)?;
let wrapped_meta_info = next_account_info(account_info_iter)?;
let bridge = Bridge::bridge_deserialize(bridge_info)?;
// Foreign chain asset, mint wrapped asset
let expected_mint_address = Bridge::derive_wrapped_asset_id(
program_id,
bridge_info.key,
a.chain,
a.decimals,
a.address,
)?;
if expected_mint_address != *mint_info.key {
return Err(Error::InvalidDerivedAccount.into());
}
// Create wrapped mint
Self::create_wrapped_mint(
program_id,
accounts,
&bridge.config.token_program,
mint_info.key,
bridge_info.key,
payer_info.key,
&a,
a.decimals,
)?;
// Check and create wrapped asset meta to allow reverse resolution of info
let wrapped_meta_seeds = Bridge::derive_wrapped_meta_seeds(bridge_info.key, mint_info.key);
Bridge::check_and_create_account::<WrappedAssetMeta>(
program_id,
accounts,
wrapped_meta_info.key,
payer_info.key,
program_id,
&wrapped_meta_seeds,
)?;
let mut wrapped_meta_data = wrapped_meta_info.data.borrow_mut();
let wrapped_meta: &mut WrappedAssetMeta = Bridge::unpack_unchecked(&mut wrapped_meta_data)?;
wrapped_meta.is_initialized = true;
wrapped_meta.address = a.address;
wrapped_meta.chain = a.chain;
Ok(())
}
} }
/// Implementation of actions /// Implementation of actions
@ -1030,7 +1060,7 @@ impl Bridge {
mint, mint,
payer, payer,
token_program, token_program,
&Self::derive_wrapped_asset_seeds(bridge, asset.chain, asset.address), &Self::derive_wrapped_asset_seeds(bridge, asset.chain, asset.decimals, asset.address),
)?; )?;
let ix = spl_token::instruction::initialize_mint( let ix = spl_token::instruction::initialize_mint(
token_program, token_program,

View File

@ -285,12 +285,14 @@ impl Bridge {
pub fn derive_wrapped_asset_seeds( pub fn derive_wrapped_asset_seeds(
bridge_key: &Pubkey, bridge_key: &Pubkey,
asset_chain: u8, asset_chain: u8,
asset_decimal: u8,
asset: ForeignAddress, asset: ForeignAddress,
) -> Vec<Vec<u8>> { ) -> Vec<Vec<u8>> {
vec![ vec![
"wrapped".as_bytes().to_vec(), "wrapped".as_bytes().to_vec(),
bridge_key.to_bytes().to_vec(), bridge_key.to_bytes().to_vec(),
asset_chain.as_bytes().to_vec(), asset_chain.as_bytes().to_vec(),
asset_decimal.as_bytes().to_vec(),
asset.as_bytes().to_vec(), asset.as_bytes().to_vec(),
] ]
} }
@ -413,11 +415,12 @@ impl Bridge {
program_id: &Pubkey, program_id: &Pubkey,
bridge_key: &Pubkey, bridge_key: &Pubkey,
asset_chain: u8, asset_chain: u8,
asset_decimal: u8,
asset: ForeignAddress, asset: ForeignAddress,
) -> Result<Pubkey, Error> { ) -> Result<Pubkey, Error> {
Ok(Self::derive_key( Ok(Self::derive_key(
program_id, program_id,
&Self::derive_wrapped_asset_seeds(bridge_key, asset_chain, asset), &Self::derive_wrapped_asset_seeds(bridge_key, asset_chain, asset_decimal, asset),
)? )?
.0) .0)
} }

View File

@ -1024,12 +1024,21 @@ fn main() {
.required(true) .required(true)
.help("Chain ID of the asset"), .help("Chain ID of the asset"),
) )
.arg(
Arg::with_name("decimals")
.validator(is_u8)
.value_name("DECIMALS")
.takes_value(true)
.index(3)
.required(true)
.help("Decimals of the asset"),
)
.arg( .arg(
Arg::with_name("token") Arg::with_name("token")
.validator(is_hex) .validator(is_hex)
.value_name("TOKEN_ADDRESS") .value_name("TOKEN_ADDRESS")
.takes_value(true) .takes_value(true)
.index(3) .index(4)
.required(true) .required(true)
.help("Token address of the asset"), .help("Token address of the asset"),
) )
@ -1162,6 +1171,7 @@ fn main() {
("wrapped-address", Some(arg_matches)) => { ("wrapped-address", Some(arg_matches)) => {
let bridge = pubkey_of(arg_matches, "bridge").unwrap(); let bridge = pubkey_of(arg_matches, "bridge").unwrap();
let chain = value_t_or_exit!(arg_matches, "chain", u8); let chain = value_t_or_exit!(arg_matches, "chain", u8);
let decimals = value_t_or_exit!(arg_matches, "decimals", u8);
let addr_string: String = value_of(arg_matches, "token").unwrap(); let addr_string: String = value_of(arg_matches, "token").unwrap();
let addr_data = hex::decode(addr_string).unwrap(); let addr_data = hex::decode(addr_string).unwrap();
@ -1170,7 +1180,8 @@ fn main() {
let bridge_key = Bridge::derive_bridge_id(&bridge).unwrap(); let bridge_key = Bridge::derive_bridge_id(&bridge).unwrap();
let wrapped_key = let wrapped_key =
Bridge::derive_wrapped_asset_id(&bridge, &bridge_key, chain, token_addr).unwrap(); Bridge::derive_wrapped_asset_id(&bridge, &bridge_key, chain, decimals, token_addr)
.unwrap();
println!("Wrapped address: {}", wrapped_key); println!("Wrapped address: {}", wrapped_key);
return; return;
} }

View File

@ -42,7 +42,7 @@ echo "Created token account $account"
cli mint "$token" 10000000000 "$account" cli mint "$token" 10000000000 "$account"
# Do lock transactions <3 # Do lock transactions <3
while : ; do #while : ; do
cli lock "$bridge_address" "$account" "$token" 10 "$chain_id_ethereum" "$RANDOM" "$recipient_address" # cli lock "$bridge_address" "$account" "$token" 10 "$chain_id_ethereum" "$RANDOM" "$recipient_address"
sleep 5 sleep 5000
done #done

2006
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,35 +3,37 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@project-serum/sol-wallet-adapter": "^0.1.0", "@project-serum/sol-wallet-adapter": "^0.1.1",
"@solana/spl-token": "^0.0.5", "@solana/spl-token": "^0.0.11",
"@solana/web3.js": "^0.71.4", "@solana/web3.js": "^0.80.2",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.5",
"@types/bs58": "^4.0.1", "@types/bs58": "^4.0.1",
"antd": "^4.4.1", "@types/jest": "^24.0.0",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^12.12.67",
"@types/react": "^16.9.52",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.6",
"antd": "^4.7.0",
"bs58": "^4.0.1",
"buffer": "^5.6.0", "buffer": "^5.6.0",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"ethers": "^4.0.44", "ethers": "^4.0.48",
"lodash.debounce": "^4.0.8",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.1", "react-scripts": "^3.4.3",
"typescript": "~3.7.2", "typescript": "~3.7.2",
"web3": "^1.2.9", "web3": "^1.3.0"
"bs58": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"npm": "^6.14.6", "@typechain/ethers-v4": "^1.0.0",
"yarn": "^1.22.4", "npm": "^6.14.8",
"typechain": "^2.0.0", "typechain": "^2.0.1",
"@typechain/ethers-v4": "^1.0.0" "yarn": "^1.22.10"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@ -11,6 +11,7 @@ import TransferSolana from "../pages/TransferSolana";
import WalletContext from '../providers/WalletContext'; import WalletContext from '../providers/WalletContext';
import Wallet from "@project-serum/sol-wallet-adapter"; import Wallet from "@project-serum/sol-wallet-adapter";
import {BridgeProvider} from "../providers/BridgeContext"; import {BridgeProvider} from "../providers/BridgeContext";
import Assistant from "../pages/Assistant";
const {Header, Content, Footer} = Layout; const {Header, Content, Footer} = Layout;
@ -37,6 +38,7 @@ function App() {
<Layout style={{height: '100%'}}> <Layout style={{height: '100%'}}>
<Router> <Router>
<Header style={{position: 'fixed', zIndex: 1, width: '100%'}}> <Header style={{position: 'fixed', zIndex: 1, width: '100%'}}>
<Link to="/assistant" style={{paddingRight: 20}}>Assistant</Link>
<Link to="/" style={{paddingRight: 20}}>Ethereum</Link> <Link to="/" style={{paddingRight: 20}}>Ethereum</Link>
<Link to="/solana">Solana</Link> <Link to="/solana">Solana</Link>
<div className="logo"/> <div className="logo"/>
@ -50,6 +52,9 @@ function App() {
<SolanaTokenProvider> <SolanaTokenProvider>
<Switch> <Switch>
<Route path="/assistant">
<Assistant/>
</Route>
<Route path="/solana"> <Route path="/solana">
<TransferSolana/> <TransferSolana/>
</Route> </Route>

View File

@ -0,0 +1,304 @@
import React, {useContext, useEffect, useState} from "react";
import {Button, Empty, Form, Input, message, Modal, Select} from "antd";
import solanaWeb3, {Account, Connection, PublicKey, Transaction} from "@solana/web3.js";
import ClientContext from "../providers/ClientContext";
import {SlotContext} from "../providers/SlotContext";
import {SolanaTokenContext} from "../providers/SolanaTokenContext";
import {BridgeContext} from "../providers/BridgeContext";
import {WrappedAssetFactory} from "../contracts/WrappedAssetFactory";
import {WalletOutlined} from '@ant-design/icons';
import {BRIDGE_ADDRESS} from "../config";
import {WormholeFactory} from "../contracts/WormholeFactory";
import {ethers} from "ethers";
import debounce from "lodash.debounce"
import BN from "bignumber.js";
import {BigNumber} from "ethers/utils";
import {AssetMeta, SolanaBridge} from "../utils/bridge";
import KeyContext from "../providers/KeyContext";
import {ChainID} from "../pages/Assistant";
const {confirm} = Modal;
const {Option} = Select;
interface TransferInitiatorParams {
onFromNetworkChanged?: (v: ChainID) => void
dataChanged?: (d: TransferInitiatorData) => void
}
export interface CoinInfo {
address: string,
name: string,
balance: BigNumber,
decimals: number,
allowance: BigNumber,
isWrapped: boolean,
chainID: number,
assetAddress: Buffer,
mint: string,
}
export interface TransferInitiatorData {
fromNetwork: ChainID,
fromCoinInfo: CoinInfo
toNetwork: ChainID,
toAddress: Buffer,
amount: BigNumber,
}
// @ts-ignore
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
export const defaultCoinInfo = {
address: "",
name: "",
balance: new BigNumber(0),
decimals: 0,
allowance: new BigNumber(0),
isWrapped: false,
chainID: 0,
assetAddress: new Buffer(0),
mint: ""
}
let debounceUpdater = debounce((e) => e(), 500)
async function createWrapped(c: Connection, b: SolanaBridge, key: Account, meta: AssetMeta, mint: PublicKey) {
try {
let tx = new Transaction();
// @ts-ignore
let [ix_account, newSigner] = await b.createWrappedAssetAndAccountInstructions(key.publicKey, mint, meta);
let recentHash = await c.getRecentBlockhash();
tx.recentBlockhash = recentHash.blockhash
tx.add(...ix_account)
tx.sign(key, newSigner)
message.loading({content: "Waiting for transaction to be confirmed...", key: "tx", duration: 1000})
await c.sendTransaction(tx, [key, newSigner], {preflightCommitment: "single"})
message.success({content: "Creation succeeded!", key: "tx"})
} catch (e) {
console.log(e)
message.error({content: "Creation failed", key: "tx"})
}
}
export default function TransferInitiator(params: TransferInitiatorParams) {
let c = useContext<solanaWeb3.Connection>(ClientContext);
let slot = useContext(SlotContext);
let b = useContext(SolanaTokenContext);
let bridge = useContext(BridgeContext);
let k = useContext(KeyContext);
let [fromNetwork, setFromNetwork] = useState(ChainID.ETH);
let [toNetwork, setToNetwork] = useState(ChainID.SOLANA);
let [fromAddress, setFromAddress] = useState("");
let [fromAddressValid, setFromAddressValid] = useState(false)
let [coinInfo, setCoinInfo] = useState<CoinInfo>(defaultCoinInfo);
let [toAddress, setToAddress] = useState("");
let [toAddressValid, setToAddressValid] = useState(false)
let [amount, setAmount] = useState(new BigNumber(0));
let [amountValid, setAmountValid] = useState(true);
let [wrappedMint, setWrappedMint] = useState("")
const updateBalance = async () => {
if (fromNetwork == ChainID.SOLANA) {
let acc = b.balances.find(value => value.account.toString() == fromAddress)
if (!acc) {
setFromAddressValid(false);
setCoinInfo(defaultCoinInfo);
return
}
console.log(acc.assetMeta)
setCoinInfo({
address: fromAddress,
name: "",
balance: acc.balance,
allowance: new BigNumber(0),
decimals: acc.assetMeta.decimals,
isWrapped: acc.assetMeta.chain != ChainID.SOLANA,
chainID: acc.assetMeta.chain,
assetAddress: acc.assetMeta.address,
// Solana specific
mint: acc.mint,
})
setFromAddressValid(true);
} else {
try {
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, BRIDGE_ADDRESS);
let info = {
address: fromAddress,
name: symbol,
balance: balance,
allowance: allowance,
decimals: decimals,
isWrapped: false,
chainID: 2,
assetAddress: new Buffer(fromAddress.slice(2), "hex"),
mint: "",
}
let b = WormholeFactory.connect(BRIDGE_ADDRESS, provider);
let isWrapped = await b.isWrappedAsset(fromAddress)
if (isWrapped) {
info.chainID = await e.assetChain()
info.assetAddress = new Buffer((await e.assetAddress()).slice(2), "hex")
info.isWrapped = true
}
let wrappedMint = await bridge.getWrappedAssetMint({
chain: info.chainID,
address: info.assetAddress,
decimals: Math.min(decimals, 9),
});
console.log(decimals)
setWrappedMint(wrappedMint.toString())
setCoinInfo(info)
setFromAddressValid(true)
} catch (e) {
setCoinInfo(defaultCoinInfo);
setFromAddressValid(false)
}
}
}
useEffect(() => {
debounceUpdater(updateBalance)
}, [fromNetwork, fromAddress])
useEffect(() => {
if (toNetwork == ChainID.ETH) {
setToAddressValid(toAddress.length == 42 && toAddress.match(/0[xX][0-9a-fA-F]+/) != null)
} else {
setToAddressValid(toAddress != "")
}
}, [toNetwork, toAddress])
useEffect(() => {
setAmountValid(amount.lte(coinInfo.balance) && amount.gt(0))
}, [amount])
useEffect(() => {
if (params.dataChanged) {
params.dataChanged({
fromCoinInfo: coinInfo,
fromNetwork,
toNetwork,
toAddress: toAddressValid ? (toNetwork == ChainID.ETH ? new Buffer(toAddress.slice(2), "hex") : new PublicKey(toAddress).toBuffer()) : new Buffer(0),
amount: amount,
});
}
}, [fromNetwork, fromAddressValid, coinInfo, toNetwork, toAddress, toAddressValid, amount])
return (
<>
<Form layout={"vertical"}>
<Form.Item label="From" name="layout" validateStatus={fromAddressValid ? "success" : "error"}>
<Input.Group compact={true}>
<Select style={{width: '30%'}} defaultValue={ChainID.ETH} className="select-before"
value={fromNetwork}
onChange={(v) => {
setFromNetwork(v);
setFromAddress("");
if (v === toNetwork) {
setToNetwork(v == ChainID.ETH ? ChainID.SOLANA : ChainID.ETH);
}
if (params.onFromNetworkChanged) params.onFromNetworkChanged(v);
}}>
<Option value={ChainID.ETH}>Ethereum</Option>
<Option value={ChainID.SOLANA}>Solana</Option>
</Select>
{fromNetwork == ChainID.ETH &&
<Input style={{width: '70%'}} placeholder="ERC20 address"
onChange={(e) => setFromAddress(e.target.value)}
suffix={coinInfo.name}/>}
{fromNetwork == ChainID.SOLANA &&
<>
<Select style={{width: '70%'}} placeholder="Pick a token account"
onChange={(e) => {
setFromAddress(e.toString())
}}>
{b.balances.map((v) => <Option
value={v.account.toString()}>{v.account.toString()}</Option>)}
</Select>
</>
}
</Input.Group>
</Form.Item>
<Form.Item label="Amount" name="layout"
validateStatus={amountValid ? "success" : "error"}>
<Input type={"number"} placeholder={"Amount"}
addonAfter={`Balance: ${coinInfo.balance.div(Math.pow(10, coinInfo.decimals))}`}
onChange={(v) => {
if (v.target.value === "") {
setAmount(new BigNumber(0));
return
}
setAmount(new BigNumber(new BN(v.target.value).multipliedBy(new BN(Math.pow(10, coinInfo.decimals))).toFixed(0)))
}}/>
</Form.Item>
<Form.Item label="Recipient" name="layout" validateStatus={toAddressValid ? "success" : "error"}>
<Input.Group compact={true}>
<Select style={{width: '30%'}} defaultValue={ChainID.SOLANA} className="select-before"
value={toNetwork}
onChange={(v) => {
setToNetwork(v)
if (v === fromNetwork) {
setFromNetwork(v == ChainID.ETH ? ChainID.SOLANA : ChainID.ETH);
}
setToAddress("");
}}>
<Option value={ChainID.ETH}>Ethereum</Option>
<Option value={ChainID.SOLANA}>Solana</Option>
</Select>
{toNetwork == ChainID.ETH &&
<Input style={{width: '70%'}} placeholder="Account address"
onChange={(e) => setToAddress(e.target.value)}/>}
{toNetwork == ChainID.SOLANA &&
<>
<Select style={{width: '60%'}} onChange={(e) => setToAddress(e.toString())}
placeholder="Pick a token account or create a new one"
notFoundContent={<Empty description="No accounts. Create a new one."/>}>
{b.balances.filter((v) => v.mint == wrappedMint).map((v) =>
<Option
value={v.account.toString()}>{v.account.toString()}</Option>)}
</Select>
<Button style={{width: '10%'}} disabled={!fromAddressValid} onClick={() => {
confirm({
title: 'Do you want to create a new token account?',
icon: <WalletOutlined/>,
content: (<>This will create a new token account for the
token: <code>{wrappedMint}</code></>),
onOk() {
console.log(coinInfo.decimals)
createWrapped(c, bridge, k, {
chain: coinInfo.chainID,
address: coinInfo.assetAddress,
decimals: Math.min(coinInfo.decimals, 9)
}, new PublicKey(wrappedMint))
},
onCancel() {
console.log('Cancel');
},
})
}}>+</Button>
</>
}
</Input.Group>
</Form.Item>
</Form>
</>
);
}

412
web/src/pages/Assistant.tsx Normal file
View File

@ -0,0 +1,412 @@
import React, {useContext, useState} from 'react';
import ClientContext from "../providers/ClientContext";
import * as solanaWeb3 from '@solana/web3.js';
import {PublicKey, Transaction} from '@solana/web3.js';
import {Button, message, Progress, Space, Spin, Steps} from "antd";
import {ethers} from "ethers";
import {Erc20Factory} from "../contracts/Erc20Factory";
import {Arrayish, BigNumber, BigNumberish} from "ethers/utils";
import {WormholeFactory} from "../contracts/WormholeFactory";
import {BRIDGE_ADDRESS, TOKEN_PROGRAM} from "../config";
import {SolanaTokenContext} from "../providers/SolanaTokenContext";
import {BridgeContext} from "../providers/BridgeContext";
import KeyContext from "../providers/KeyContext";
import TransferInitiator, {defaultCoinInfo, TransferInitiatorData} from "../components/TransferInitiator";
import * as spl from "@solana/spl-token";
import BN from "bn.js"
import {SlotContext} from "../providers/SlotContext";
const {Step} = Steps;
// @ts-ignore
window.ethereum.enable();
// @ts-ignore
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
interface LoadingInfo {
loading: boolean,
message: string,
progress?: ProgressInfo,
}
interface ProgressInfo {
completion: number,
content: string
}
export enum ChainID {
SOLANA = 1,
ETH
}
async function approveAssets(asset: string,
amount: BigNumberish) {
let e = Erc20Factory.connect(asset, signer);
try {
message.loading({content: "Signing transaction...", key: "eth_tx", duration: 1000})
let res = await e.approve(BRIDGE_ADDRESS, amount)
message.loading({content: "Waiting for transaction to be mined...", key: "eth_tx", duration: 1000})
await res.wait(1);
message.success({content: "Approval on ETH succeeded!", key: "eth_tx"})
} catch (e) {
message.error({content: "Approval failed", key: "eth_tx"})
}
}
function Assistant() {
let c = useContext<solanaWeb3.Connection>(ClientContext);
let tokenAccounts = useContext(SolanaTokenContext);
let bridge = useContext(BridgeContext);
let k = useContext(KeyContext);
let slot = useContext(SlotContext);
let [fromNetwork, setFromNetwork] = useState(ChainID.ETH)
let [transferData, setTransferData] = useState<TransferInitiatorData>({
fromCoinInfo: defaultCoinInfo,
fromNetwork: 0,
toNetwork: 0,
toAddress: new Buffer(0),
amount: new BigNumber(0),
});
let [loading, setLoading] = useState<LoadingInfo>({
loading: false,
message: "",
progress: undefined
})
let [current, setCurrent] = useState(0);
let nextStep = (from: string) => {
setLoading({
...loading,
loading: false
})
if (from == "approve") {
lockAssets(transferData.fromCoinInfo?.address, transferData.amount, transferData.toAddress, transferData.toNetwork)
} else if (from == "lock") {
// Await approvals or allow to submit guardian shit
if (fromNetwork == ChainID.ETH && transferData.toNetwork == ChainID.SOLANA) {
awaitCompletionEth()
} else if (fromNetwork == ChainID.SOLANA && transferData.toNetwork == ChainID.ETH) {
awaitCompletionSolana()
}
} else if (from == "vaa") {
postVAAOnEth()
}
setCurrent((v) => v + 1)
}
const lockAssets = async function (asset: string,
amount: BigNumberish,
recipient: Arrayish,
target_chain: BigNumberish) {
let wh = WormholeFactory.connect(BRIDGE_ADDRESS, signer);
try {
setLoading({
...loading,
loading: true,
message: "Allow transfer in Metamask...",
})
let res = await wh.lockAssets(asset, amount, recipient, target_chain, 10, false)
setLoading({
...loading,
loading: true,
message: "Waiting for transaction to be mined...",
})
await res.wait(1);
message.success({content: "Transfer on ETH succeeded!", key: "eth_tx"})
nextStep("lock");
} catch (e) {
message.error({content: "Transfer failed", key: "eth_tx"})
setCurrent(0);
setLoading({
...loading,
loading: false,
})
}
}
const approveAssets = async function (asset: string,
amount: BigNumberish) {
let e = Erc20Factory.connect(asset, signer);
try {
setLoading({
...loading,
loading: true,
message: "Allow approval in Metamask...",
})
let res = await e.approve(BRIDGE_ADDRESS, amount)
setLoading({
...loading,
loading: true,
message: "Waiting for transaction to be mined...",
})
await res.wait(1);
message.success({content: "Approval on ETH succeeded!", key: "eth_tx"})
nextStep("approve")
} catch (e) {
message.error({content: "Approval failed", key: "eth_tx"})
setCurrent(0);
setLoading({
loading: false,
...loading
})
}
}
const initiateTransfer = () => {
if (fromNetwork == ChainID.ETH && transferData.fromCoinInfo) {
nextStep("init")
if (transferData.fromCoinInfo?.allowance.lt(transferData.amount)) {
approveAssets(transferData.fromCoinInfo?.address, transferData.amount)
} else {
lockAssets(transferData.fromCoinInfo?.address, transferData.amount, transferData.toAddress, transferData.toNetwork)
}
} else if (fromNetwork == ChainID.SOLANA && transferData.fromCoinInfo) {
nextStep("init")
solanaTransfer();
}
}
let transferProposal: PublicKey;
let transferVAA = new Uint8Array(0);
const solanaTransfer = async () => {
setLoading({
...loading,
loading: true,
message: "Locking tokens on Solana...",
})
let {ix: lock_ix, transferKey} = await bridge.createLockAssetInstruction(k.publicKey, new PublicKey(transferData.fromCoinInfo.address),
new PublicKey(transferData.fromCoinInfo.mint), new BN(transferData.amount.toString()),
transferData.toNetwork, transferData.toAddress,
{
chain: transferData.fromCoinInfo.chainID,
address: transferData.fromCoinInfo.assetAddress,
decimals: transferData.fromCoinInfo.decimals,
}, Math.random() * 100000);
let ix = spl.Token.createApproveInstruction(TOKEN_PROGRAM, new PublicKey(transferData.fromCoinInfo.address), await bridge.getConfigKey(), k.publicKey, [], transferData.amount.toNumber())
let recentHash = await c.getRecentBlockhash();
let tx = new Transaction();
tx.recentBlockhash = recentHash.blockhash
tx.add(ix)
tx.add(lock_ix)
tx.sign(k)
try {
await c.sendTransaction(tx, [k])
message.success({content: "Transfer succeeded", key: "transfer"})
} catch (e) {
message.error({content: "Transfer failed", key: "transfer"})
}
transferProposal = transferKey
nextStep("lock")
}
const executeVAAOnETH = async (vaa: Uint8Array) => {
let wh = WormholeFactory.connect(BRIDGE_ADDRESS, signer)
setLoading({
...loading,
loading: true,
message: "Sign the claim...",
})
let tx = await wh.submitVAA(vaa)
setLoading({
...loading,
loading: true,
message: "Waiting for tokens unlock to be mined...",
})
await tx.wait(1)
message.success({content: "Execution of VAA succeeded", key: "eth_tx"})
nextStep("submit")
}
const awaitCompletionEth = () => {
let startBlock = provider.blockNumber;
let completed = false;
let blockHandler = (blockNumber: number) => {
if (blockNumber - startBlock < 5) {
setLoading({
loading: true,
message: "Awaiting ETH confirmations",
progress: {
completion: (blockNumber - startBlock) / 5 * 100,
content: `${blockNumber - startBlock}/${5}`
}
})
} else if (!completed) {
provider.removeListener("block", blockHandler)
setLoading({loading: true, message: "Awaiting completion on Solana"})
}
}
provider.on("block", blockHandler)
let accountChangeListener = c.onAccountChange(new PublicKey(transferData.toAddress), () => {
if (completed) return;
completed = true;
provider.removeListener("block", blockHandler)
c.removeAccountChangeListener(accountChangeListener);
nextStep("await")
}, "single")
}
const awaitCompletionSolana = () => {
let completed = false;
let startSlot = slot;
let slotUpdateListener = c.onSlotChange((slot) => {
if (completed) return;
if (slot.slot - startSlot < 32) {
setLoading({
loading: true,
message: "Awaiting confirmations",
progress: {
completion: (slot.slot - startSlot) / 32 * 100,
content: `${slot.slot - startSlot}/${32}`
}
})
} else {
setLoading({loading: true, message: "Awaiting guardians (TODO ping)"})
}
})
let accountChangeListener = c.onAccountChange(transferProposal, async (a) => {
if (completed) return;
let lockup = bridge.parseLockup(transferProposal, a.data);
let vaa = lockup.vaa;
console.log(lockup)
for (let i = vaa.length; i > 0; i--) {
if (vaa[i] == 0xff) {
vaa = vaa.slice(0, i)
break
}
}
// Probably a poke
if (vaa.filter(v => v != 0).length == 0) {
return
}
completed = true;
c.removeAccountChangeListener(accountChangeListener);
c.removeSlotChangeListener(slotUpdateListener);
let signatures = await bridge.fetchSignatureStatus(lockup.signatureAccount);
let sigData = Buffer.of(...signatures.reduce((previousValue, currentValue) => {
previousValue.push(currentValue.index)
previousValue.push(...currentValue.signature)
return previousValue
}, new Array<number>()))
vaa = Buffer.concat([vaa.slice(0, 5), Buffer.of(signatures.length), sigData, vaa.slice(6)])
transferVAA = vaa
nextStep("vaa")
}, "single")
}
const postVAAOnEth = () => {
executeVAAOnETH(transferVAA);
}
const steps = [
{
title: 'Initiate Transfer',
content: (
<>
<TransferInitiator onFromNetworkChanged={setFromNetwork} dataChanged={(d) => {
setTransferData(d);
}}/>
<Button onClick={initiateTransfer}>Transfer</Button>
</>),
},
];
if (fromNetwork == ChainID.ETH) {
if (transferData.fromCoinInfo && transferData.fromCoinInfo.allowance.lt(transferData.amount)) {
steps.push({
title: 'Approval',
content: (<></>),
})
}
steps.push(...[
{
title: 'Transfer',
content: (<></>),
},
{
title: 'Wait for confirmations',
content: (<></>),
},
{
title: 'Done',
content: (<><Space align="center" style={{width: "100%", paddingTop: "128px", paddingBottom: "128px"}}
direction="vertical">
<Progress type="circle" percent={100} format={() => 'Done'}/>
<b>Your transfer has been completed</b>
</Space></>),
},
])
} else {
steps.push(...[
{
title: 'Transfer',
content: (<></>),
},
{
title: 'Wait for approval',
content: (<></>),
},
{
title: 'Claim tokens on ETH',
content: (<></>),
},
{
title: 'Done',
content: (<><Space align="center" style={{width: "100%", paddingTop: "128px", paddingBottom: "128px"}}
direction="vertical">
<Progress type="circle" percent={100} format={() => 'Done'}/>
<b>Your transfer has been completed</b>
</Space></>),
},
])
}
return (
<>
<Steps current={current}>
{steps.map(item => (
<Step key={item.title} title={item.title}/>
))}
</Steps>
<div className="steps-content"
style={{marginTop: "24px", marginBottom: "24px"}}>
{loading.loading ? loading.progress ? (
<Space align="center" style={{width: "100%", paddingTop: "128px", paddingBottom: "128px"}}
direction="vertical">
<ProgressIndicator {...loading.progress}/>
<b>{loading.message}</b>
</Space>) :
<Space align="center" style={{width: "100%", paddingTop: "128px", paddingBottom: "128px"}}
direction="vertical">
<Spin size={"large"}/>
<b>{loading.message}</b>
</Space> : steps[current].content}
</div>
</>
);
}
let ProgressIndicator = (params: { completion: number, content: string }) => {
return (<Progress type="circle" percent={params.completion} format={() => params.content}/>)
}
export default Assistant;

View File

@ -60,7 +60,7 @@ async function createWrapped(c: Connection, b: SolanaBridge, key: Account, meta:
let tx = new Transaction(); let tx = new Transaction();
// @ts-ignore // @ts-ignore
let [ix_account, newSigner] = await b.createWrappedAssetAndAccountInstructions(key.publicKey, mint); let [ix_account, newSigner] = await b.createWrappedAssetAndAccountInstructions(key.publicKey, mint, meta);
let recentHash = await c.getRecentBlockhash(); let recentHash = await c.getRecentBlockhash();
tx.recentBlockhash = recentHash.blockhash tx.recentBlockhash = recentHash.blockhash
tx.add(...ix_account) tx.add(...ix_account)
@ -69,6 +69,7 @@ async function createWrapped(c: Connection, b: SolanaBridge, key: Account, meta:
await c.sendTransaction(tx, [key, newSigner]) await c.sendTransaction(tx, [key, newSigner])
message.success({content: "Creation succeeded!", key: "tx"}) message.success({content: "Creation succeeded!", key: "tx"})
} catch (e) { } catch (e) {
console.log(e)
message.error({content: "Creation failed", key: "tx"}) message.error({content: "Creation failed", key: "tx"})
} }
} }
@ -152,7 +153,8 @@ function Transfer() {
let getWrappedInfo = async () => { let getWrappedInfo = async () => {
let wrappedMint = await bridge.getWrappedAssetMint({ let wrappedMint = await bridge.getWrappedAssetMint({
chain: coinInfo.chainID, chain: coinInfo.chainID,
address: coinInfo.assetAddress address: coinInfo.assetAddress,
decimals: Math.min(coinInfo.decimals, 9)
}); });
setWrappedMint(wrappedMint.toString()) setWrappedMint(wrappedMint.toString())
@ -241,7 +243,8 @@ function Transfer() {
onClick={() => { onClick={() => {
createWrapped(c, bridge, k, { createWrapped(c, bridge, k, {
chain: coinInfo.chainID, chain: coinInfo.chainID,
address: coinInfo.assetAddress address: coinInfo.assetAddress,
decimals: Math.min(coinInfo.decimals, 9)
}, new PublicKey(wrappedMint)) }, new PublicKey(wrappedMint))
}}>Create new</Button></Col> }}>Create new</Button></Col>
</Row> </Row>

View File

@ -3,7 +3,7 @@ import ClientContext from "../providers/ClientContext";
import * as solanaWeb3 from '@solana/web3.js'; import * as solanaWeb3 from '@solana/web3.js';
import {PublicKey, Transaction} from '@solana/web3.js'; import {PublicKey, Transaction} from '@solana/web3.js';
import * as spl from '@solana/spl-token'; import * as spl from '@solana/spl-token';
import {Button, Card, Col, Divider, Form, Input, InputNumber, List, message, Row, Select} from "antd"; import {Button, Col, Form, Input, InputNumber, message, Row, Select} from "antd";
import {BigNumber} from "ethers/utils"; import {BigNumber} from "ethers/utils";
import SplBalances from "../components/SplBalances"; import SplBalances from "../components/SplBalances";
import {SlotContext} from "../providers/SlotContext"; import {SlotContext} from "../providers/SlotContext";
@ -56,6 +56,7 @@ function TransferSolana() {
getCoinInfo() getCoinInfo()
}, [address]) }, [address])
return ( return (
<> <>
<Row gutter={12}> <Row gutter={12}>
@ -70,11 +71,12 @@ function TransferSolana() {
let send = async () => { let send = async () => {
message.loading({content: "Transferring tokens...", key: "transfer"}, 1000) message.loading({content: "Transferring tokens...", key: "transfer"}, 1000)
let lock_ix = await bridge.createLockAssetInstruction(k.publicKey, fromAccount, new PublicKey(coinInfo.mint), transferAmount, values["target_chain"], recipient, let {ix: lock_ix} = await bridge.createLockAssetInstruction(k.publicKey, fromAccount, new PublicKey(coinInfo.mint), transferAmount, values["target_chain"], recipient,
{ {
chain: coinInfo.chainID, chain: coinInfo.chainID,
address: coinInfo.wrappedAddress address: coinInfo.wrappedAddress,
}, Math.random()*100000); decimals: Math.min(coinInfo.decimals, 9)
}, Math.random() * 100000);
let ix = spl.Token.createApproveInstruction(TOKEN_PROGRAM, fromAccount, await bridge.getConfigKey(), k.publicKey, [], transferAmount.toNumber()) let ix = spl.Token.createApproveInstruction(TOKEN_PROGRAM, fromAccount, await bridge.getConfigKey(), k.publicKey, [], transferAmount.toNumber())
let recentHash = await c.getRecentBlockhash(); let recentHash = await c.getRecentBlockhash();
@ -91,8 +93,9 @@ function TransferSolana() {
} }
} }
send() send()
}} layout={"vertical"} > }} layout={"vertical"}>
<Form.Item name="address" validateStatus={addressValid ? "success" : "error"} label={"Token Account:"}> <Form.Item name="address" validateStatus={addressValid ? "success" : "error"}
label={"Token Account:"}>
<Input <Input
addonAfter={`Balance: ${coinInfo.balance.div(new BigNumber(Math.pow(10, coinInfo.decimals)))}`} addonAfter={`Balance: ${coinInfo.balance.div(new BigNumber(Math.pow(10, coinInfo.decimals)))}`}
name="address" name="address"

View File

@ -48,6 +48,7 @@ export const SolanaTokenProvider: FunctionComponent = ({children}) => {
if (!am) { if (!am) {
throw new Error("could not derive asset meta") throw new Error("could not derive asset meta")
} }
am.decimals = acc.account.data.parsed.info.tokenAmount.decimals;
meta.push(am) meta.push(am)
} }
let balances: Array<BalanceInfo> = await res.value.map((v, i) => { let balances: Array<BalanceInfo> = await res.value.map((v, i) => {

View File

@ -2,14 +2,16 @@ import * as solanaWeb3 from "@solana/web3.js";
import {PublicKey, TransactionInstruction} from "@solana/web3.js"; import {PublicKey, TransactionInstruction} from "@solana/web3.js";
import BN from 'bn.js'; import BN from 'bn.js';
import assert from "assert"; import assert from "assert";
import * as spl from '@solana/spl-token';
import {Token} from '@solana/spl-token';
// @ts-ignore // @ts-ignore
import * as BufferLayout from 'buffer-layout' import * as BufferLayout from 'buffer-layout'
import {Token} from "@solana/spl-token"; import {SOLANA_BRIDGE_PROGRAM, SOLANA_HOST, TOKEN_PROGRAM} from "../config";
import {SOLANA_HOST, TOKEN_PROGRAM} from "../config";
import * as bs58 from "bs58"; import * as bs58 from "bs58";
export interface AssetMeta { export interface AssetMeta {
chain: number, chain: number,
decimals: number,
address: Buffer address: Buffer
} }
@ -57,7 +59,7 @@ class SolanaBridge {
targetAddress: Buffer, targetAddress: Buffer,
asset: AssetMeta, asset: AssetMeta,
nonce: number, nonce: number,
): Promise<TransactionInstruction> { ): Promise<{ ix: TransactionInstruction, transferKey: PublicKey }> {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'), BufferLayout.u8('instruction'),
uint256('amount'), uint256('amount'),
@ -90,7 +92,7 @@ class SolanaBridge {
targetChain: targetChain, targetChain: targetChain,
assetAddress: padBuffer(asset.address, 32), assetAddress: padBuffer(asset.address, 32),
assetChain: asset.chain, assetChain: asset.chain,
assetDecimals: 0, // This is fetched on chain assetDecimals: asset.decimals,
targetAddress: padBuffer(targetAddress, 32), targetAddress: padBuffer(targetAddress, 32),
nonce: nonce, nonce: nonce,
}, },
@ -107,7 +109,7 @@ class SolanaBridge {
{pubkey: configKey, isSigner: false, isWritable: false}, {pubkey: configKey, isSigner: false, isWritable: false},
{pubkey: transferKey, isSigner: false, isWritable: true}, {pubkey: transferKey, isSigner: false, isWritable: true},
{pubkey: mint, isSigner: false, isWritable: false}, {pubkey: mint, isSigner: false, isWritable: true},
{pubkey: payer, isSigner: true, isWritable: true}, {pubkey: payer, isSigner: true, isWritable: true},
]; ];
@ -118,11 +120,14 @@ class SolanaBridge {
} }
return new TransactionInstruction({ return {
keys, ix: new TransactionInstruction({
programId: this.programID, keys,
data, programId: this.programID,
}); data,
}),
transferKey: transferKey,
};
} }
createPokeProposalInstruction( createPokeProposalInstruction(
@ -163,6 +168,7 @@ class SolanaBridge {
return { return {
address: mint.toBuffer(), address: mint.toBuffer(),
chain: CHAIN_ID_SOLANA, chain: CHAIN_ID_SOLANA,
decimals: 0,
} }
} else { } else {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
@ -173,7 +179,8 @@ class SolanaBridge {
return { return {
address: wrappedMeta.assetAddress, address: wrappedMeta.assetAddress,
chain: wrappedMeta.assetChain chain: wrappedMeta.assetChain,
decimals: 0
} }
} }
} }
@ -182,8 +189,7 @@ class SolanaBridge {
async fetchSignatureStatus( async fetchSignatureStatus(
signatureStatus: PublicKey, signatureStatus: PublicKey,
): Promise<Signature[]> { ): Promise<Signature[]> {
let signatureInfo = await this.connection.getAccountInfo(signatureStatus); let signatureInfo = await this.connection.getAccountInfo(signatureStatus, "single");
console.log(signatureStatus.toBase58())
if (signatureInfo == null || signatureInfo.lamports == 0) { if (signatureInfo == null || signatureInfo.lamports == 0) {
throw new Error("not found") throw new Error("not found")
} else { } else {
@ -214,6 +220,46 @@ class SolanaBridge {
} }
} }
parseLockup(address: PublicKey, data: Buffer): Lockup {
const dataLayout = BufferLayout.struct([
uint256('amount'),
BufferLayout.u8('toChain'),
BufferLayout.blob(32, 'sourceAddress'),
BufferLayout.blob(32, 'targetAddress'),
BufferLayout.blob(32, 'assetAddress'),
BufferLayout.u8('assetChain'),
BufferLayout.u8('assetDecimals'),
BufferLayout.seq(BufferLayout.u8(), 1), // 4 byte alignment because a u32 is following
BufferLayout.u32('nonce'),
BufferLayout.blob(1001, 'vaa'),
BufferLayout.seq(BufferLayout.u8(), 3), // 4 byte alignment because a u32 is following
BufferLayout.u32('vaaTime'),
BufferLayout.u32('lockupTime'),
BufferLayout.u8('pokeCounter'),
BufferLayout.blob(32, 'signatureAccount'),
BufferLayout.u8('initialized'),
]);
let parsedAccount = dataLayout.decode(data)
return {
lockupAddress: address,
amount: new BN(parsedAccount.amount, 2, "le"),
assetAddress: parsedAccount.assetAddress,
assetChain: parsedAccount.assetChain,
assetDecimals: parsedAccount.assetDecimals,
initialized: parsedAccount.initialized == 1,
nonce: parsedAccount.nonce,
sourceAddress: new PublicKey(parsedAccount.sourceAddress),
targetAddress: parsedAccount.targetAddress,
toChain: parsedAccount.toChain,
vaa: parsedAccount.vaa,
vaaTime: parsedAccount.vaaTime,
signatureAccount: new PublicKey(parsedAccount.signatureAccount),
pokeCounter: parsedAccount.pokeCounter
}
}
// fetchAssetMeta fetches the AssetMeta for an SPL token // fetchAssetMeta fetches the AssetMeta for an SPL token
async fetchTransferProposals( async fetchTransferProposals(
tokenAccount: PublicKey, tokenAccount: PublicKey,
@ -240,44 +286,9 @@ class SolanaBridge {
}) })
let raw_accounts = (await accountRes.json())["result"]; let raw_accounts = (await accountRes.json())["result"];
const dataLayout = BufferLayout.struct([
uint256('amount'),
BufferLayout.u8('toChain'),
BufferLayout.blob(32, 'sourceAddress'),
BufferLayout.blob(32, 'targetAddress'),
BufferLayout.blob(32, 'assetAddress'),
BufferLayout.u8('assetChain'),
BufferLayout.u8('assetDecimals'),
BufferLayout.seq(BufferLayout.u8(), 1), // 4 byte alignment because a u32 is following
BufferLayout.u32('nonce'),
BufferLayout.blob(1001, 'vaa'),
BufferLayout.seq(BufferLayout.u8(), 3), // 4 byte alignment because a u32 is following
BufferLayout.u32('vaaTime'),
BufferLayout.u32('lockupTime'),
BufferLayout.u8('pokeCounter'),
BufferLayout.blob(32, 'signatureAccount'),
BufferLayout.u8('initialized'),
]);
let accounts: Lockup[] = []; let accounts: Lockup[] = [];
for (let acc of raw_accounts) { for (let acc of raw_accounts) {
let parsedAccount = dataLayout.decode(bs58.decode(acc.account.data)) accounts.push(this.parseLockup(acc.pubkey, bs58.decode(acc.account.data)))
accounts.push({
lockupAddress: acc.pubkey,
amount: new BN(parsedAccount.amount, 2, "le"),
assetAddress: parsedAccount.assetAddress,
assetChain: parsedAccount.assetChain,
assetDecimals: parsedAccount.assetDecimals,
initialized: parsedAccount.initialized == 1,
nonce: parsedAccount.nonce,
sourceAddress: new PublicKey(parsedAccount.sourceAddress),
targetAddress: parsedAccount.targetAddress,
toChain: parsedAccount.toChain,
vaa: parsedAccount.vaa,
vaaTime: parsedAccount.vaaTime,
signatureAccount: new PublicKey(parsedAccount.signatureAccount),
pokeCounter: parsedAccount.pokeCounter
})
} }
return accounts return accounts
@ -285,16 +296,16 @@ class SolanaBridge {
AccountLayout = BufferLayout.struct([publicKey('mint'), publicKey('owner'), uint64('amount'), BufferLayout.u32('option'), publicKey('delegate'), BufferLayout.u8('is_initialized'), BufferLayout.u8('is_native'), BufferLayout.u16('padding'), uint64('delegatedAmount')]); AccountLayout = BufferLayout.struct([publicKey('mint'), publicKey('owner'), uint64('amount'), BufferLayout.u32('option'), publicKey('delegate'), BufferLayout.u8('is_initialized'), BufferLayout.u8('is_native'), BufferLayout.u16('padding'), uint64('delegatedAmount')]);
async createWrappedAssetAndAccountInstructions(owner: PublicKey, mint: PublicKey): Promise<[TransactionInstruction[], solanaWeb3.Account]> { async createWrappedAssetAndAccountInstructions(owner: PublicKey, mint: PublicKey, meta: AssetMeta): Promise<[TransactionInstruction[], solanaWeb3.Account]> {
const newAccount = new solanaWeb3.Account(); const newAccount = new solanaWeb3.Account();
// @ts-ignore // @ts-ignore
const balanceNeeded = await Token.getMinBalanceRentForExemptAccount(this.connection); const balanceNeeded = await Token.getMinBalanceRentForExemptAccount(this.connection);
let transaction = solanaWeb3.SystemProgram.createAccount({ let create_ix = solanaWeb3.SystemProgram.createAccount({
fromPubkey: owner, fromPubkey: owner,
newAccountPubkey: newAccount.publicKey, newAccountPubkey: newAccount.publicKey,
lamports: balanceNeeded, lamports: balanceNeeded,
space: this.AccountLayout.span, space: spl.AccountLayout.span,
programId: TOKEN_PROGRAM, programId: TOKEN_PROGRAM,
}); // create the new account }); // create the new account
@ -310,6 +321,10 @@ class SolanaBridge {
pubkey: owner, pubkey: owner,
isSigner: false, isSigner: false,
isWritable: false isWritable: false
}, {
pubkey: solanaWeb3.SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false
}]; }];
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]); const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
@ -323,7 +338,61 @@ class SolanaBridge {
data data
} }
return [[transaction.instructions[0], ix_init], newAccount] let ixs: TransactionInstruction[] = [];
let configKey = await this.getConfigKey();
let wrappedKey = await this.getWrappedAssetMint(meta);
let wrappedAcc = await this.connection.getAccountInfo(wrappedKey, "single");
if (!wrappedAcc) {
let metaKey = await this.getWrappedAssetMeta(wrappedKey);
const wa_keys = [{
pubkey: solanaWeb3.SystemProgram.programId,
isSigner: false,
isWritable: false
}, {
pubkey: TOKEN_PROGRAM,
isSigner: false,
isWritable: false
}, {
pubkey: solanaWeb3.SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false
}, {
pubkey: configKey,
isSigner: false,
isWritable: false
}, {
pubkey: owner,
isSigner: true,
isWritable: true
}, {
pubkey: wrappedKey,
isSigner: false,
isWritable: true
}, {
pubkey: metaKey,
isSigner: false,
isWritable: true
}];
const wrappedDataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.blob(32, "assetAddress"), BufferLayout.u8('chain'), BufferLayout.u8('decimals')]);
const wrappedData = Buffer.alloc(wrappedDataLayout.span);
wrappedDataLayout.encode({
instruction: 7, // CreateWrapped instruction
assetAddress: padBuffer(meta.address, 32),
chain: meta.chain,
decimals: meta.decimals
}, wrappedData);
let ix_wrapped = {
keys: wa_keys,
programId: SOLANA_BRIDGE_PROGRAM,
data: wrappedData
}
ixs.push(ix_wrapped);
}
ixs.push(create_ix, ix_init)
return [ixs, newAccount]
} }
async getConfigKey(): Promise<PublicKey> { async getConfigKey(): Promise<PublicKey> {
@ -337,11 +406,18 @@ class SolanaBridge {
} }
let configKey = await this.getConfigKey(); let configKey = await this.getConfigKey();
let seeds: Array<Buffer> = [Buffer.from("wrapped"), configKey.toBuffer(), Buffer.of(asset.chain), let seeds: Array<Buffer> = [Buffer.from("wrapped"), configKey.toBuffer(), Buffer.of(asset.chain), Buffer.of(asset.decimals),
padBuffer(asset.address, 32)]; padBuffer(asset.address, 32)];
// @ts-ignore // @ts-ignore
return (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0]; return (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0];
} }
async getWrappedAssetMeta(mint: PublicKey): Promise<PublicKey> {
let configKey = await this.getConfigKey();
let seeds: Array<Buffer> = [Buffer.from("meta"), configKey.toBuffer(), mint.toBuffer()];
// @ts-ignore
return (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0];
}
} }
// Taken from https://github.com/solana-labs/solana-program-library // Taken from https://github.com/solana-labs/solana-program-library

File diff suppressed because it is too large Load Diff