Skip to content

Commit

Permalink
omniTransfer for NEAR (#13)
Browse files Browse the repository at this point in the history
* omniTransfer for NEAR

* Clean up types
  • Loading branch information
r-near authored Dec 20, 2024
1 parent 7895491 commit b1b61f4
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 69 deletions.
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

0 comments on commit b1b61f4

Please sign in to comment.