From b1b61f4c0babb93d05d0eed9e20d092b3cde9baa Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Fri, 20 Dec 2024 02:35:15 -0800 Subject: [PATCH] omniTransfer for NEAR (#13) * omniTransfer for NEAR * Clean up types --- README.md | 14 ++-- src/api.ts | 8 ++- src/client.ts | 68 +++++++----------- src/deployer/near.ts | 161 +++++++++++++++++++++++++++++++++++++++++-- src/types/omni.ts | 19 ++--- 5 files changed, 201 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 99ee6d8..773220c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/api.ts b/src/api.ts index fbf07fc..3533010 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,5 @@ // api.ts -import { type ChainKind, type OmniAddress, Status } from "./types" +import type { ChainKind, OmniAddress } from "./types" export interface ApiTransferResponse { id: { @@ -29,6 +29,12 @@ export type ApiFee = { nativeFee: bigint } +export enum Status { + Pending = 0, + Completed = 1, + Failed = 2, +} + export class OmniBridgeAPI { private baseUrl: string diff --git a/src/client.ts b/src/client.ts index 3439940..555441a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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 { + if (wallet instanceof EthWallet) { + throw new Error("Ethereum wallet not supported") } - async omniTransfer(transferMessage: TransferMessage): Promise { - 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 { - // 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 { - // 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 { - // 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") } diff --git a/src/deployer/near.ts b/src/deployer/near.ts index 5b4d3f0..d331c62 100644 --- a/src/deployer/near.ts +++ b/src/deployer/near.ts @@ -1,6 +1,7 @@ import { borshSerialize } from "borsher" import type { Account } from "near-api-js" import { + type AccountId, type BindTokenArgs, ChainKind, type DeployTokenArgs, @@ -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 { /** @@ -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 { // Validate source chain is NEAR if (getChain(tokenAddress) !== ChainKind.Near) { @@ -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 { const proverArgs: WormholeVerifyProofArgs = { proof_kind: ProofKind.DeployToken, @@ -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, @@ -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 { + 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 + } + } } diff --git a/src/types/omni.ts b/src/types/omni.ts index a833b0d..01139cf 100644 --- a/src/types/omni.ts +++ b/src/types/omni.ts @@ -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 {