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:
parent
a0b3b1bf0c
commit
e266bf1a7c
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
15052
web/yarn.lock
15052
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue