Skip to content

Commit

Permalink
Support NEAR & EVM finTransfer (#17)
Browse files Browse the repository at this point in the history
* Support NEAR finTransfer

* Remove this

* Fix broken test

* Add tests for near fin transfer

* Add EVM finTransfer
  • Loading branch information
r-near authored Jan 3, 2025
1 parent 9bca865 commit 7d22bf8
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 34 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ console.log(`Status: ${status}`); // 'pending' | 'completed' | 'failed'
#### Core Transfer Interface

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

#### Query Functions
Expand Down
56 changes: 55 additions & 1 deletion src/deployer/evm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ethers } from "ethers"
import type { ChainKind, MPCSignature, OmniAddress, TokenMetadata, U128 } from "../types"
import type {
BridgeDeposit,
ChainKind,
MPCSignature,
OmniAddress,
TokenMetadata,
TransferMessagePayload,
U128,
} from "../types"
import { getChain } from "../utils"

// Type helpers for EVM chains
Expand Down Expand Up @@ -193,4 +201,50 @@ export class EVMDeployer {
)
}
}

/**
* Finalizes a transfer on the EVM chain by minting/unlocking tokens.
* @param transferMessage - The transfer message payload from NEAR
* @param signature - MPC signature authorizing the transfer
* @returns Promise resolving to the transaction hash
*/
async finalizeTransfer(
transferMessage: TransferMessagePayload,
signature: MPCSignature,
): Promise<string> {
// Convert the transfer message to EVM-compatible format
const bridgeDeposit: BridgeDeposit = {
destination_nonce: transferMessage.destination_nonce,
origin_chain: Number(transferMessage.transfer_id.origin_chain),
origin_nonce: transferMessage.transfer_id.origin_nonce,
token_address: this.extractEvmAddress(transferMessage.token_address),
amount: BigInt(transferMessage.amount),
recipient: this.extractEvmAddress(transferMessage.recipient),
fee_recipient: transferMessage.fee_recipient ?? "",
}

try {
const tx = await this.factory.finTransfer(signature.toBytes(true), bridgeDeposit)
const receipt = await tx.wait()
return receipt.hash
} catch (error) {
throw new Error(
`Failed to finalize transfer: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}

/**
* Helper method to extract EVM address from OmniAddress
* @param omniAddress - The OmniAddress to extract from
* @returns The EVM address
*/
private extractEvmAddress(omniAddress: OmniAddress): string {
const chain = getChain(omniAddress)
const [_, address] = omniAddress.split(":")
if (!ChainUtils.isEVMChain(chain)) {
throw new Error(`Invalid EVM address: ${omniAddress}`)
}
return address
}
}
138 changes: 120 additions & 18 deletions src/deployer/near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import type { Account } from "near-api-js"
import {
type AccountId,
type BindTokenArgs,
BindTokenArgsSchema,
ChainKind,
type DeployTokenArgs,
DeployTokenArgsSchema,
type EvmVerifyProofArgs,
EvmVerifyProofArgsSchema,
type FinTransferArgs,
FinTransferArgsSchema,
type LogMetadataArgs,
type OmniAddress,
ProofKind,
Expand All @@ -27,6 +30,7 @@ const GAS = {
DEPLOY_TOKEN: BigInt(1.2e14), // 1.2 TGas
BIND_TOKEN: BigInt(3e14), // 3 TGas
INIT_TRANSFER: BigInt(3e14), // 3 TGas
FIN_TRANSFER: BigInt(3e14), // 3 TGas
STORAGE_DEPOSIT: BigInt(1e14), // 1 TGas
} as const

Expand All @@ -40,6 +44,7 @@ const DEPOSIT = {
DEPLOY_TOKEN: BigInt(4e24), // 4 NEAR
BIND_TOKEN: BigInt(2e23), // 0.2 NEAR
INIT_TRANSFER: BigInt(1), // 1 yoctoNEAR
FIN_TRANSFER: BigInt(1), // 1 yoctoNEAR
} as const

/**
Expand Down Expand Up @@ -67,11 +72,15 @@ interface InitTransferMessage {
* Interface representing the results of various balance queries
* @property regBalance - Required balance for account registration
* @property initBalance - Required balance for initializing transfers
* @property finBalance - Required balance for finalizing transfers
* @property bindBalance - Required balance for binding tokens
* @property storage - Current storage deposit balance information
*/
interface BalanceResults {
regBalance: bigint
initBalance: bigint
finBalance: bigint
bindBalance: bigint
storage: StorageDeposit
}

Expand Down Expand Up @@ -203,10 +212,11 @@ export class NearDeployer {
chain_kind: sourceChain,
prover_args: proverArgsSerialized,
}
const serializedArgs = borshSerialize(BindTokenArgsSchema, args)
const tx = await this.wallet.functionCall({
contractId: this.lockerAddress,
methodName: "bind_token",
args,
args: serializedArgs,
gas: GAS.BIND_TOKEN,
attachedDeposit: DEPOSIT.BIND_TOKEN,
})
Expand Down Expand Up @@ -276,6 +286,81 @@ export class NearDeployer {
}
}

/**
* Finalizes a cross-chain token transfer on NEAR by processing the transfer proof and managing storage deposits.
* Supports both Wormhole VAA and EVM proof verification for transfers from supported chains.
*
* @param token - The token identifier on NEAR where transferred tokens will be minted
* @param account - The recipient account ID on NEAR
* @param storageDepositAmount - Amount of NEAR tokens for storage deposit (in yoctoNEAR)
* @param sourceChain - The originating chain of the transfer
* @param vaa - Optional Wormhole Verified Action Approval containing transfer information
* @param evmProof - Optional proof data for transfers from EVM-compatible chains
*
* @throws {Error} When neither VAA nor EVM proof is provided
* @throws {Error} When EVM proof is provided for non-EVM chains (only valid for Ethereum, Arbitrum, or Base)
*
* @returns Promise resolving to the finalization transaction hash
*
*/
async finalizeTransfer(
token: string,
account: string,
storageDepositAmount: U128,
sourceChain: ChainKind,
vaa?: string,
evmProof?: EvmVerifyProofArgs,
): Promise<string> {
if (!vaa && !evmProof) {
throw new Error("Must provide either VAA or EVM proof")
}
if (evmProof) {
if (
sourceChain !== ChainKind.Eth &&
sourceChain !== ChainKind.Arb &&
sourceChain !== ChainKind.Base
) {
throw new Error("EVM proof is only valid for Ethereum, Arbitrum, or Base")
}
}
let proverArgsSerialized: Uint8Array = new Uint8Array(0)
if (vaa) {
const proverArgs: WormholeVerifyProofArgs = {
proof_kind: ProofKind.DeployToken,
vaa: vaa,
}
proverArgsSerialized = borshSerialize(WormholeVerifyProofArgsSchema, proverArgs)
} else if (evmProof) {
const proverArgs: EvmVerifyProofArgs = {
proof_kind: ProofKind.DeployToken,
proof: evmProof.proof,
}
proverArgsSerialized = borshSerialize(EvmVerifyProofArgsSchema, proverArgs)
}

const args: FinTransferArgs = {
chain_kind: sourceChain,
storage_deposit_actions: [
{
token_id: token,
account_id: account,
storage_deposit_amount: storageDepositAmount,
},
],
prover_args: proverArgsSerialized,
}
const serializedArgs = borshSerialize(FinTransferArgsSchema, args)

const tx = await this.wallet.functionCall({
contractId: this.lockerAddress,
methodName: "finalize_transfer",
args: serializedArgs,
gas: GAS.FIN_TRANSFER,
attachedDeposit: DEPOSIT.FIN_TRANSFER,
})
return tx.transaction.hash
}

/**
* Retrieves various balance information for the current account
* @private
Expand All @@ -284,23 +369,38 @@ export class NearDeployer {
*/
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,
},
}),
])
const [regBalanceStr, initBalanceStr, finBalanceStr, bindBalanceStr, 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: "required_balance_for_fin_transfer",
args: {
account_id: this.wallet.accountId,
},
}),
this.wallet.viewFunction({
contractId: this.lockerAddress,
methodName: "required_balance_for_bind_token",
args: {
account_id: this.wallet.accountId,
},
}),
this.wallet.viewFunction({
contractId: this.lockerAddress,
methodName: "storage_balance_of",
args: {
account_id: this.wallet.accountId,
},
}),
])

// Convert storage balance to bigint
let convertedStorage = null
Expand All @@ -314,6 +414,8 @@ export class NearDeployer {
return {
regBalance: BigInt(regBalanceStr),
initBalance: BigInt(initBalanceStr),
finBalance: BigInt(finBalanceStr),
bindBalance: BigInt(bindBalanceStr),
storage: convertedStorage,
}
} catch (error) {
Expand Down
1 change: 0 additions & 1 deletion src/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export type U128 = bigint
export type Nonce = bigint
export type TransferId = string
export type AccountId = string
export type Fee = bigint
export type OmniAddress =
Expand Down
34 changes: 34 additions & 0 deletions src/types/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ChainKind } from "./chain"
import type { Nonce, OmniAddress, U128 } from "./common"

export enum PayloadType {
TransferMessage = "TransferMessage",
Metadata = "Metadata",
ClaimNativeFee = "ClaimNativeFee",
}

export interface TransferId {
origin_chain: ChainKind
origin_nonce: Nonce
}

// bridge deposit structure for evm chains
export type BridgeDeposit = {
destination_nonce: Nonce
origin_chain: number // u8 in rust
origin_nonce: Nonce
token_address: string // evm address
amount: bigint // uint128 in solidity
recipient: string // evm address
fee_recipient: string
}

export type TransferMessagePayload = {
prefix: PayloadType
destination_nonce: Nonce
transfer_id: TransferId
token_address: OmniAddress
amount: U128
recipient: OmniAddress
fee_recipient: string | null // NEAR AccountId or null
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./locker"
export * from "./omni"
export * from "./prover"
export * from "./mpc"
export * from "./evm"
4 changes: 2 additions & 2 deletions src/types/prover.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BorshSchema } from "borsher"
import type { AccountId, Fee, Nonce, OmniAddress, TransferId, U128 } from "./common"
import type { AccountId, Fee, Nonce, OmniAddress, U128 } from "./common"

export enum ProofKind {
InitTransfer = 0,
Expand Down Expand Up @@ -38,7 +38,7 @@ export const InitTransferMessageSchema = BorshSchema.Struct({
})

export type FinTransferMessage = {
transfer_id: TransferId
transfer_id: string
fee_recipient: AccountId
amount: U128
emitter_address: OmniAddress
Expand Down
Loading

0 comments on commit 7d22bf8

Please sign in to comment.