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

Support NEAR & EVM finTransfer #17

Merged
merged 5 commits into from
Jan 3, 2025
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
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
Loading