215 lines
7.5 KiB
TypeScript
215 lines
7.5 KiB
TypeScript
import Wallet from "@project-serum/sol-wallet-adapter";
|
|
import {
|
|
AccountMeta,
|
|
Connection,
|
|
PublicKey,
|
|
SystemProgram,
|
|
Transaction,
|
|
TransactionInstruction,
|
|
} from "@solana/web3.js";
|
|
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
|
import { ethers } from "ethers";
|
|
import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils";
|
|
import {
|
|
Bridge__factory,
|
|
TokenImplementation__factory,
|
|
} from "../ethers-contracts";
|
|
import {
|
|
ChainId,
|
|
CHAIN_ID_ETH,
|
|
CHAIN_ID_SOLANA,
|
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
|
SOLANA_HOST,
|
|
SOL_BRIDGE_ADDRESS,
|
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
|
} from "./consts";
|
|
|
|
// TODO: this should probably be extended from the context somehow so that the signatures match
|
|
// TODO: allow for / handle cancellation?
|
|
// TODO: overall better input checking and error handling
|
|
export function transferFromEth(
|
|
provider: ethers.providers.Web3Provider | undefined,
|
|
tokenAddress: string,
|
|
amount: string,
|
|
recipientChain: ChainId,
|
|
recipientAddress: Uint8Array | undefined
|
|
) {
|
|
if (!provider || !recipientAddress) return;
|
|
const signer = provider.getSigner();
|
|
if (!signer) return;
|
|
//TODO: check if token attestation exists on the target chain
|
|
//TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain?
|
|
//TODO: more catches
|
|
const amountParsed = parseUnits(amount, 18);
|
|
signer.getAddress().then((signerAddress) => {
|
|
console.log("Signer:", signerAddress);
|
|
console.log("Token:", tokenAddress);
|
|
const token = TokenImplementation__factory.connect(tokenAddress, signer);
|
|
token
|
|
.allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS)
|
|
.then((allowance) => {
|
|
console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't?
|
|
token
|
|
.approve(ETH_TOKEN_BRIDGE_ADDRESS, amountParsed)
|
|
.then((transaction) => {
|
|
console.log(transaction);
|
|
const fee = 0; // for now, this won't do anything, we may add later
|
|
const nonceConst = Math.random() * 100000;
|
|
const nonceBuffer = Buffer.alloc(4);
|
|
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
|
console.log("Initiating transfer");
|
|
console.log("Amount:", formatUnits(amountParsed, 18));
|
|
console.log("To chain:", recipientChain);
|
|
console.log("To address:", recipientAddress);
|
|
console.log("Fees:", fee);
|
|
console.log("Nonce:", nonceBuffer);
|
|
const bridge = Bridge__factory.connect(
|
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
|
signer
|
|
);
|
|
bridge
|
|
.transferTokens(
|
|
tokenAddress,
|
|
amountParsed,
|
|
recipientChain,
|
|
recipientAddress,
|
|
fee,
|
|
nonceBuffer
|
|
)
|
|
.then((v) => console.log("Success:", v))
|
|
.catch((r) => console.error(r)); //TODO: integrate toast messages
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// TODO: should we share this with client? ooh, should client use the SDK ;)
|
|
// begin from clients\solana\main.ts
|
|
function ixFromRust(data: any): TransactionInstruction {
|
|
let keys: Array<AccountMeta> = data.accounts.map(accountMetaFromRust);
|
|
return new TransactionInstruction({
|
|
programId: new PublicKey(data.program_id),
|
|
data: Buffer.from(data.data),
|
|
keys: keys,
|
|
});
|
|
}
|
|
|
|
function accountMetaFromRust(meta: any): AccountMeta {
|
|
return {
|
|
pubkey: new PublicKey(meta.pubkey),
|
|
isSigner: meta.is_signer,
|
|
isWritable: meta.is_writable,
|
|
};
|
|
}
|
|
// end from clients\solana\main.ts
|
|
|
|
// TODO: need to check transfer native vs transfer wrapped
|
|
// TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts)
|
|
export function transferFromSolana(
|
|
wallet: Wallet | undefined,
|
|
payerAddress: string | undefined, //TODO: we may not need this since we have wallet
|
|
fromAddress: string | undefined,
|
|
mintAddress: string,
|
|
amount: string,
|
|
decimals: number,
|
|
targetProvider: ethers.providers.Web3Provider | undefined,
|
|
targetChain: ChainId
|
|
) {
|
|
if (
|
|
!wallet ||
|
|
!wallet.publicKey ||
|
|
!payerAddress ||
|
|
!fromAddress ||
|
|
!targetProvider
|
|
)
|
|
return;
|
|
const targetSigner = targetProvider.getSigner();
|
|
if (!targetSigner) return;
|
|
(async () => {
|
|
const targetAddressStr = await targetSigner.getAddress();
|
|
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
|
|
const nonceConst = Math.random() * 100000;
|
|
const nonceBuffer = Buffer.alloc(4);
|
|
nonceBuffer.writeUInt32LE(nonceConst, 0);
|
|
const nonce = nonceBuffer.readUInt32LE(0);
|
|
const amountParsed = parseUnits(amount, decimals).toBigInt();
|
|
const fee = BigInt(0); // for now, this won't do anything, we may add later
|
|
console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS);
|
|
console.log("bridge:", SOL_BRIDGE_ADDRESS);
|
|
console.log("payer:", payerAddress);
|
|
console.log("from:", fromAddress);
|
|
console.log("token:", mintAddress);
|
|
console.log("nonce:", nonce);
|
|
console.log("amount:", amountParsed);
|
|
console.log("fee:", fee);
|
|
console.log("target:", targetAddressStr, targetAddress);
|
|
console.log("chain:", targetChain);
|
|
const bridge = await import("bridge");
|
|
const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS);
|
|
const bridgeStatePK = new PublicKey(
|
|
bridge.state_address(SOL_BRIDGE_ADDRESS)
|
|
);
|
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
|
const bridgeStateAccountInfo = await connection.getAccountInfo(
|
|
bridgeStatePK
|
|
);
|
|
if (bridgeStateAccountInfo?.data === undefined) {
|
|
throw new Error("bridge state not found");
|
|
}
|
|
const bridgeState = bridge.parse_state(
|
|
new Uint8Array(bridgeStateAccountInfo?.data)
|
|
);
|
|
const transferIx = SystemProgram.transfer({
|
|
fromPubkey: new PublicKey(payerAddress),
|
|
toPubkey: new PublicKey(feeAccount),
|
|
lamports: bridgeState.config.fee,
|
|
});
|
|
// TODO: pass in connection
|
|
// Add transfer instruction to transaction
|
|
const { transfer_native_ix, approval_authority_address } = await import(
|
|
"token-bridge"
|
|
);
|
|
const approvalIx = Token.createApproveInstruction(
|
|
TOKEN_PROGRAM_ID,
|
|
new PublicKey(fromAddress),
|
|
new PublicKey(approval_authority_address(SOL_TOKEN_BRIDGE_ADDRESS)),
|
|
new PublicKey(payerAddress),
|
|
[],
|
|
Number(amountParsed)
|
|
);
|
|
const ix = ixFromRust(
|
|
transfer_native_ix(
|
|
SOL_TOKEN_BRIDGE_ADDRESS,
|
|
SOL_BRIDGE_ADDRESS,
|
|
payerAddress,
|
|
fromAddress,
|
|
mintAddress,
|
|
nonce,
|
|
amountParsed,
|
|
fee,
|
|
targetAddress,
|
|
targetChain
|
|
)
|
|
);
|
|
console.log(ix);
|
|
const transaction = new Transaction().add(transferIx, approvalIx, ix);
|
|
const { blockhash } = await connection.getRecentBlockhash();
|
|
transaction.recentBlockhash = blockhash;
|
|
transaction.feePayer = new PublicKey(payerAddress);
|
|
// Sign transaction, broadcast, and confirm
|
|
const signed = await wallet.signTransaction(transaction);
|
|
console.log("SIGNED", signed);
|
|
const txid = await connection.sendRawTransaction(signed.serialize());
|
|
console.log("SENT", txid);
|
|
await connection.confirmTransaction(txid);
|
|
console.log("CONFIRMED");
|
|
})();
|
|
}
|
|
|
|
const transferFrom = {
|
|
[CHAIN_ID_ETH]: transferFromEth,
|
|
[CHAIN_ID_SOLANA]: transferFromSolana,
|
|
};
|
|
|
|
export default transferFrom;
|