From 9bca865f3ebed0c112da19b9f218f495ad55d1c5 Mon Sep 17 00:00:00 2001 From: kiseln <3428059+kiseln@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:39:45 +0400 Subject: [PATCH] Implement init transfer for all chains (#16) --- src/client.ts | 42 ++++++++++++---- src/deployer/evm.ts | 53 +++++++++++++++++--- src/deployer/near.ts | 14 +++--- src/deployer/solana.ts | 111 ++++++++++++++++++++++++++++++++++++++++- src/types/mpc.ts | 4 +- 5 files changed, 197 insertions(+), 27 deletions(-) diff --git a/src/client.ts b/src/client.ts index 555441a..ac6b6e5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,17 +1,16 @@ -import type { Signer as SolWallet } from "@solana/web3.js" +import { AnchorProvider as SolWallet } from "@coral-xyz/anchor" +import { PublicKey } from "@solana/web3.js" import { Wallet as EthWallet } from "ethers" import { Account as NearAccount } from "near-api-js" +import { EVMDeployer } from "./deployer/evm" import { NearDeployer } from "./deployer/near" -import type { OmniTransferMessage, OmniTransferResult } from "./types" +import { SolanaDeployer } from "./deployer/solana" +import { ChainKind, type OmniTransferMessage, type OmniTransferResult } from "./types" export async function omniTransfer( wallet: EthWallet | NearAccount | SolWallet, transfer: OmniTransferMessage, ): Promise { - if (wallet instanceof EthWallet) { - throw new Error("Ethereum wallet not supported") - } - if (wallet instanceof NearAccount) { const deployer = new NearDeployer(wallet, "omni-locker.testnet") // TODO: Get from config const { nonce, hash } = await deployer.initTransfer( @@ -25,10 +24,33 @@ export async function omniTransfer( } } - if ("publicKey" in wallet) { - // Solana wallet check - // Solana transfer implementation - throw new Error("Solana wallet not supported") + if (wallet instanceof EthWallet) { + const deployer = new EVMDeployer(wallet, ChainKind.Eth) + const { hash, nonce } = await deployer.initTransfer( + transfer.tokenAddress, + transfer.recipient, + transfer.amount, + ) + return { + txId: hash, + nonce: BigInt(nonce), + } + } + + if (wallet instanceof SolWallet) { + const deployer = new SolanaDeployer( + wallet, + new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"), + ) // TODO: Get from config + const { nonce, hash } = await deployer.initTransfer( + transfer.tokenAddress, + transfer.recipient, + transfer.amount, + ) + return { + txId: hash, + nonce: BigInt(nonce), + } } throw new Error("Unsupported wallet type") diff --git a/src/deployer/evm.ts b/src/deployer/evm.ts index 4c48f06..c1673c0 100644 --- a/src/deployer/evm.ts +++ b/src/deployer/evm.ts @@ -1,5 +1,5 @@ import { ethers } from "ethers" -import type { ChainKind, MPCSignature, OmniAddress, TokenMetadata } from "../types" +import type { ChainKind, MPCSignature, OmniAddress, TokenMetadata, U128 } from "../types" import { getChain } from "../utils" // Type helpers for EVM chains @@ -116,10 +116,6 @@ export class EVMDeployer { }) return tx.hash } catch (error) { - // Check if error message contains revert string - if (error instanceof Error && error.message.includes("DEFAULT_ADMIN_ROLE")) { - throw new Error("Failed to log metadata: Caller does not have admin role") - } throw new Error( `Failed to log metadata: ${error instanceof Error ? error.message : "Unknown error"}`, ) @@ -140,7 +136,7 @@ export class EVMDeployer { txHash: string tokenAddress: string }> { - const tx = await this.factory.deployToken(signature.toBytes(), metadata, { + const tx = await this.factory.deployToken(signature.toBytes(true), metadata, { gasLimit: GAS_LIMIT.DEPLOY_TOKEN[this.chainTag], }) @@ -152,4 +148,49 @@ export class EVMDeployer { tokenAddress: deployedAddress, } } + + /** + * Transfers ERC-20 tokens to the bridge contract on the EVM chain. + * This transaction generates a proof that is subsequently used to mint/unlock + * corresponding tokens on the destination chain. + * + * @param token - Omni address of the ERC20 token to transfer + * @param recipient - Recipient's Omni address on the destination chain where tokens will be minted + * @param amount - Amount of the tokens to transfer + * @throws {Error} If token address is not on the correct EVM chain + * @returns Promise resolving to object containing transaction hash and nonce + */ + async initTransfer( + token: OmniAddress, + recipient: OmniAddress, + amount: U128, + ): Promise<{ hash: string; nonce: number }> { + const sourceChain = getChain(token) + + // Validate source chain matches the deployer's chain + if (!ChainUtils.areEqual(sourceChain, this.chainKind)) { + throw new Error(`Token address must be on ${this.chainTag}`) + } + + const [_, tokenAccountId] = token.split(":") + + try { + const tx = await this.factory.initTransfer( + tokenAccountId, + amount.valueOf(), + 0, + 0, + recipient, + "", + ) + return { + hash: tx.hash, + nonce: 0, + } + } catch (error) { + throw new Error( + `Failed to init transfer: ${error instanceof Error ? error.message : "Unknown error"}`, + ) + } + } } diff --git a/src/deployer/near.ts b/src/deployer/near.ts index 0049611..f54b9e3 100644 --- a/src/deployer/near.ts +++ b/src/deployer/near.ts @@ -53,14 +53,14 @@ type StorageDeposit = { interface TransferMessage { receiver_id: AccountId memo: string | null - amount: U128 + amount: string msg: string | null } interface InitTransferMessage { recipient: OmniAddress - fee: U128 - native_token_fee: U128 + fee: string + native_token_fee: string } /** @@ -229,7 +229,7 @@ export class NearDeployer { async initTransfer( token: OmniAddress, recipient: OmniAddress, - amount: bigint, + amount: U128, ): Promise<{ hash: string; nonce: number }> { if (getChain(token) !== ChainKind.Near) { throw new Error("Token address must be on NEAR") @@ -253,12 +253,12 @@ export class NearDeployer { const initTransferMessage: InitTransferMessage = { recipient: recipient, - fee: BigInt(0), - native_token_fee: BigInt(0), + fee: "0", + native_token_fee: "0", } const args: TransferMessage = { receiver_id: this.lockerAddress, - amount: amount, + amount: amount.toString(), memo: null, msg: JSON.stringify(initTransferMessage), } diff --git a/src/deployer/solana.ts b/src/deployer/solana.ts index 4ea8791..007d677 100644 --- a/src/deployer/solana.ts +++ b/src/deployer/solana.ts @@ -1,15 +1,23 @@ -import { Program, type Provider } from "@coral-xyz/anchor" +import { BN, Program, type Provider } from "@coral-xyz/anchor" import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token" import { Keypair, + type ParsedAccountData, PublicKey, SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SystemProgram, } from "@solana/web3.js" -import type { MPCSignature, TokenMetadata } from "../types" +import { + ChainKind, + type MPCSignature, + type OmniAddress, + type TokenMetadata, + type U128, +} from "../types" import type { BridgeTokenFactory } from "../types/solana/bridge_token_factory" import BRIDGE_TOKEN_FACTORY_IDL from "../types/solana/bridge_token_factory.json" +import { getChain } from "../utils" const MPL_PROGRAM_ID = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s") @@ -32,6 +40,7 @@ export class SolanaDeployer { AUTHORITY: this.getConstant("AUTHORITY_SEED"), WRAPPED_MINT: this.getConstant("WRAPPED_MINT_SEED"), VAULT: this.getConstant("VAULT_SEED"), + SOL_VAULT: this.getConstant("SOL_VAULT_SEED"), } constructor(provider: Provider, wormholeProgramId: PublicKey) { @@ -85,6 +94,13 @@ export class SolanaDeployer { ) } + private solVaultId(): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [SolanaDeployer.SEEDS.SOL_VAULT], + this.program.programId, + ) + } + /** * Logs metadata for a token * @param token - The token's public key @@ -188,4 +204,95 @@ export class SolanaDeployer { throw new Error(`Failed to deploy token: ${e}`) } } + + /** + * Transfers SPL tokens to the bridge contract on Solana. + * This transaction generates a proof that is subsequently used to mint/unlock + * corresponding tokens on the destination chain. + * + * @param token - Omni address of the SPL token to transfer + * @param recipient - Recipient's Omni address on the destination chain where tokens will be minted + * @param amount - Amount of the tokens to transfer + * @throws {Error} If token address is not on Solana + * @returns Promise resolving to object containing transaction hash and nonce + */ + async initTransfer( + token: OmniAddress, + recipient: OmniAddress, + amount: U128, + payer?: Keypair, + ): Promise<{ hash: string; nonce: number }> { + if (getChain(token) !== ChainKind.Sol) { + throw new Error("Token address must be on Solana") + } + const wormholeMessage = Keypair.generate() + + const payerPubKey = payer?.publicKey || this.program.provider.publicKey + if (!payerPubKey) { + throw new Error("Payer is not configured") + } + + const mint = new PublicKey(token.split(":")[1]) + const [from] = PublicKey.findProgramAddressSync( + [payerPubKey.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], + ASSOCIATED_TOKEN_PROGRAM_ID, + ) + const vault = (await this.isBridgedToken(mint)) ? null : this.vaultId(mint)[0] + const [solVault] = this.solVaultId() + + try { + const tx = await this.program.methods + .initTransfer({ + amount: new BN(amount.valueOf()), + recipient, + fee: new BN(0), + nativeFee: new BN(0), + }) + .accountsStrict({ + authority: this.authority()[0], + mint, + from, + vault, + solVault, + user: payerPubKey, + wormhole: { + payer: payerPubKey, + config: this.config()[0], + bridge: this.wormholeBridgeId()[0], + feeCollector: this.wormholeFeeCollectorId()[0], + sequence: this.wormholeSequenceId()[0], + clock: SYSVAR_CLOCK_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + wormholeProgram: this.wormholeProgramId, + message: wormholeMessage.publicKey, + }, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers(payer instanceof Keypair ? [wormholeMessage, payer] : [wormholeMessage]) + .rpc() + + return { + hash: tx, + nonce: 0, + } + } catch (e) { + throw new Error(`Failed to init transfer: ${e}`) + } + } + + private async isBridgedToken(token: PublicKey): Promise { + const mintInfo = await this.program.provider.connection.getParsedAccountInfo(token) + + if (!mintInfo.value) { + throw new Error("Failed to find mint account") + } + + const data = mintInfo.value.data as ParsedAccountData + if (!data.parsed || data.program !== "spl-token" || data.parsed.type !== "mint") { + throw new Error("Not a valid SPL token mint") + } + + return data.parsed.info.mintAuthority.toString() === this.authority()[0].toString() + } } diff --git a/src/types/mpc.ts b/src/types/mpc.ts index 09a5a01..c731584 100644 --- a/src/types/mpc.ts +++ b/src/types/mpc.ts @@ -24,10 +24,10 @@ export class MPCSignature implements SignatureResponse { public recovery_id: number, ) {} - toBytes(): Uint8Array { + toBytes(forEvm = false): Uint8Array { const bigRBytes = fromHex(this.big_r.affine_point) const sBytes = fromHex(this.s.scalar) - const result = [...bigRBytes.slice(1), ...sBytes, this.recovery_id + 27] + const result = [...bigRBytes.slice(1), ...sBytes, this.recovery_id + (forEvm ? 27 : 0)] return new Uint8Array(result) } }