Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

omniTransfer for NEAR #13

Merged
merged 2 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ console.log(`Status: ${status}`); // 'pending' | 'completed' | 'failed'
#### Core Transfer Interface

- [ ] Base OmniTransfer interface
- [ ] Ethereum implementation
- [ ] NEAR implementation
- [ ] Solana implementation
- [ ] Arbitrum implementation
- [ ] Base implementation
- [ ] EVM
- [ ] initTransfer
- [ ] finalizeTransfer
- [ ] NEAR
- [x] initTransfer
- [ ] finalizeTransfer
- [ ] Solana
- [ ] initTransfer
- [ ] finalizeTransfer

#### Query Functions

Expand Down
8 changes: 7 additions & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// api.ts
import { type ChainKind, type OmniAddress, Status } from "./types"
import type { ChainKind, OmniAddress } from "./types"

export interface ApiTransferResponse {
id: {
Expand Down Expand Up @@ -29,6 +29,12 @@ export type ApiFee = {
nativeFee: bigint
}

export enum Status {
Pending = 0,
Completed = 1,
Failed = 2,
}

export class OmniBridgeAPI {
private baseUrl: string

Expand Down
68 changes: 23 additions & 45 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,35 @@
import type { Signer as SolWallet } from "@solana/web3.js"
import { Wallet as EthWallet } from "ethers"
import { Account as NearAccount } from "near-api-js"
import type { ChainKind, Fee, OmniAddress, OmniTransfer, Status, TransferMessage } from "./types"
import { NearDeployer } from "./deployer/near"
import type { OmniTransferMessage, OmniTransferResult } from "./types"

export class OmniClient {
private wallet: EthWallet | NearAccount | SolWallet

constructor(wallet: EthWallet | NearAccount | SolWallet) {
this.wallet = wallet
export async function omniTransfer(
wallet: EthWallet | NearAccount | SolWallet,
transfer: OmniTransferMessage,
): Promise<OmniTransferResult> {
if (wallet instanceof EthWallet) {
throw new Error("Ethereum wallet not supported")
}

async omniTransfer(transferMessage: TransferMessage): Promise<OmniTransfer> {
if (this.wallet instanceof EthWallet) {
// TODO: Transfer ETH
// Not implemented yet, return a placeholder
return {
txId: "0x123",
nonce: BigInt(1),
transferMessage,
}
}
if (this.wallet instanceof NearAccount) {
// TODO: Transfer NEAR
// Not implemented yet, return a placeholder
return {
txId: "near_tx_hash",
nonce: BigInt(1),
transferMessage,
}
if (wallet instanceof NearAccount) {
const deployer = new NearDeployer(wallet, "omni-locker.testnet") // TODO: Get from config
const { nonce, hash } = await deployer.initTransfer(
transfer.tokenAddress,
transfer.recipient,
transfer.amount,
)
return {
txId: hash,
nonce: BigInt(nonce),
}

// Handle other wallet types...
throw new Error("Unsupported wallet type")
}

// biome-ignore lint/correctness/noUnusedVariables: This is a placeholder
async findOmniTransfers(sender: OmniAddress): Promise<OmniTransfer[]> {
// Query transfers from API
// This would need to be implemented based on how transfers are stored
throw new Error("Not implemented")
if ("publicKey" in wallet) {
// Solana wallet check
// Solana transfer implementation
throw new Error("Solana wallet not supported")
}

// biome-ignore lint/correctness/noUnusedVariables: This is a placeholder
async getFee(sender: OmniAddress, recipient: OmniAddress): Promise<Fee> {
// Query fee from API
// This would need to be implemented based on how fees are determined
throw new Error("Not implemented")
}

// biome-ignore lint/correctness/noUnusedVariables: This is a placeholder
async getTransferStatus(originChain: ChainKind, nonce: bigint): Promise<Status> {
// Query transfer status from API
// This would need to be implemented based on how transfers are stored
throw new Error("Not implemented")
}
throw new Error("Unsupported wallet type")
}
161 changes: 157 additions & 4 deletions src/deployer/near.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { borshSerialize } from "borsher"
import type { Account } from "near-api-js"
import {
type AccountId,
type BindTokenArgs,
ChainKind,
type DeployTokenArgs,
Expand All @@ -10,33 +11,73 @@ import {
type LogMetadataArgs,
type OmniAddress,
ProofKind,
type U128,
type WormholeVerifyProofArgs,
WormholeVerifyProofArgsSchema,
} from "../types"
import { getChain } from "../utils"

/**
* Configuration for NEAR network gas limits
* Configuration for NEAR network gas limits.
* All values are specified in TGas (Terra Gas) units.
* @internal
*/
const GAS = {
LOG_METADATA: BigInt(3e14), // 3 TGas
DEPLOY_TOKEN: BigInt(1.2e14), // 1.2 TGas
BIND_TOKEN: BigInt(3e14), // 3 TGas
INIT_TRANSFER: BigInt(3e14), // 3 TGas
STORAGE_DEPOSIT: BigInt(1e14), // 1 TGas
} as const

/**
* Configuration for NEAR network deposit amounts
* Configuration for NEAR network deposit amounts.
* Values represent the amount of NEAR tokens required for each operation.
* @internal
*/
const DEPOSIT = {
LOG_METADATA: BigInt(2e23), // 0.2 NEAR
DEPLOY_TOKEN: BigInt(4e24), // 4 NEAR
BIND_TOKEN: BigInt(2e23), // 0.2 NEAR
INIT_TRANSFER: BigInt(1), // 1 yoctoNEAR
} as const

/**
* NEAR blockchain implementation of the token deployer
* Represents the storage deposit balance for a NEAR account
*/
type StorageDeposit = {
total: bigint
available: bigint
} | null

interface TransferMessage {
receiver_id: AccountId
memo: string | null
amount: U128
msg: string | null
}

interface InitTransferMessage {
recipient: OmniAddress
fee: U128
native_token_fee: U128
}

/**
* Interface representing the results of various balance queries
* @property regBalance - Required balance for account registration
* @property initBalance - Required balance for initializing transfers
* @property storage - Current storage deposit balance information
*/
interface BalanceResults {
regBalance: bigint
initBalance: bigint
storage: StorageDeposit
}

/**
* NEAR blockchain implementation of the token deployer.
* Handles token deployment, binding, and transfer operations on the NEAR blockchain.
*/
export class NearDeployer {
/**
Expand All @@ -54,6 +95,12 @@ export class NearDeployer {
}
}

/**
* Logs metadata for a token on the NEAR blockchain
* @param tokenAddress - Omni address of the token
* @throws {Error} If token address is not on NEAR chain
* @returns Promise resolving to the transaction hash
*/
async logMetadata(tokenAddress: OmniAddress): Promise<string> {
// Validate source chain is NEAR
if (getChain(tokenAddress) !== ChainKind.Near) {
Expand All @@ -77,6 +124,12 @@ export class NearDeployer {
return tx.transaction.hash
}

/**
* Deploys a token to the specified destination chain
* @param destinationChain - Target chain where the token will be deployed
* @param vaa - Verified Action Approval containing deployment information
* @returns Promise resolving to the transaction hash
*/
async deployToken(destinationChain: ChainKind, vaa: string): Promise<string> {
const proverArgs: WormholeVerifyProofArgs = {
proof_kind: ProofKind.DeployToken,
Expand Down Expand Up @@ -108,7 +161,8 @@ export class NearDeployer {
* @param vaa - Verified Action Approval for Wormhole verification
* @param evmProof - EVM proof for Ethereum or EVM chain verification
* @throws {Error} If VAA or EVM proof is not provided
* @returns Transaction hash of the bind token transaction
* @throws {Error} If EVM proof is provided for non-EVM chain
* @returns Promise resolving to the transaction hash
*/
async bindToken(
sourceChain: ChainKind,
Expand Down Expand Up @@ -159,4 +213,103 @@ export class NearDeployer {

return tx.transaction.hash
}

/**
* Transfers NEP-141 tokens to the token locker contract on NEAR.
* This transaction generates a proof that is subsequently used to mint
* corresponding tokens on the destination chain.
*
* @param token - Omni address of the NEP-141 token to transfer
* @param recipient - Recipient's Omni address on the destination chain where tokens will be minted
* @param amount - Amount of NEP-141 tokens to transfer
* @throws {Error} If token address is not on NEAR chain
* @returns Promise resolving to object containing transaction hash and nonce
*/

async initTransfer(
token: OmniAddress,
recipient: OmniAddress,
amount: bigint,
): Promise<{ hash: string; nonce: number }> {
if (getChain(token) !== ChainKind.Near) {
throw new Error("Token address must be on NEAR")
}
const tokenAddress = token.split(":")[1]

const { regBalance, initBalance, storage } = await this.getBalances()
const requiredBalance = regBalance + initBalance
const existingBalance = storage?.available ?? BigInt(0)

if (requiredBalance > existingBalance) {
const neededAmount = requiredBalance - existingBalance
await this.wallet.functionCall({
contractId: this.lockerAddress,
methodName: "storage_deposit",
args: {},
gas: GAS.STORAGE_DEPOSIT,
attachedDeposit: neededAmount,
})
}

const initTransferMessage: InitTransferMessage = {
recipient: recipient,
fee: BigInt(0),
native_token_fee: BigInt(0),
}
const args: TransferMessage = {
receiver_id: this.lockerAddress,
amount: amount,
memo: null,
msg: JSON.stringify(initTransferMessage),
}
const tx = await this.wallet.functionCall({
contractId: tokenAddress,
methodName: "ft_transfer_call",
args,
gas: GAS.INIT_TRANSFER,
attachedDeposit: DEPOSIT.INIT_TRANSFER,
})

return {
hash: tx.transaction.hash,
nonce: tx.transaction.nonce,
}
}

/**
* Retrieves various balance information for the current account
* @private
* @returns Promise resolving to object containing required balances and storage information
* @throws {Error} If balance fetching fails
*/
private async getBalances(): Promise<BalanceResults> {
try {
const [regBalanceStr, initBalanceStr, storage] = await Promise.all([
this.wallet.viewFunction({
contractId: this.lockerAddress,
methodName: "required_balance_for_account",
}),
this.wallet.viewFunction({
contractId: this.lockerAddress,
methodName: "required_balance_for_init_transfer",
}),
this.wallet.viewFunction({
contractId: this.lockerAddress,
methodName: "storage_balance_of",
args: {
account_id: this.wallet.accountId,
},
}),
])

return {
regBalance: BigInt(regBalanceStr),
initBalance: BigInt(initBalanceStr),
storage,
}
} catch (error) {
console.error("Error fetching balances:", error)
throw error
}
}
}
19 changes: 5 additions & 14 deletions src/types/omni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,16 @@ export type TokenDeployment = {
bindTx?: string
}

export interface TransferMessage {
export interface OmniTransferResult {
nonce: bigint
txId: string
}
export interface OmniTransferMessage {
tokenAddress: OmniAddress
amount: bigint
fee: bigint
nativeFee: bigint
recipient: OmniAddress
message: string | null
}

export interface OmniTransfer {
txId: string
nonce: bigint
transferMessage: TransferMessage
}

export enum Status {
Pending = 0,
Completed = 1,
Failed = 2,
}

export interface TokenMetadata {
Expand Down
Loading