Skip to content

Commit

Permalink
Implement init transfer for all chains (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiseln authored Jan 2, 2025
1 parent 9889a58 commit 9bca865
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 27 deletions.
42 changes: 32 additions & 10 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -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<OmniTransferResult> {
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(
Expand All @@ -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")
Expand Down
53 changes: 47 additions & 6 deletions src/deployer/evm.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"}`,
)
Expand All @@ -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],
})

Expand All @@ -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"}`,
)
}
}
}
14 changes: 7 additions & 7 deletions src/deployer/near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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")
Expand All @@ -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),
}
Expand Down
111 changes: 109 additions & 2 deletions src/deployer/solana.ts
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<boolean> {
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()
}
}
4 changes: 2 additions & 2 deletions src/types/mpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit 9bca865

Please sign in to comment.