From 50689c937466811d2e509d17b8892e8c9109186c Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:03:02 -0800 Subject: [PATCH 1/5] Support NEAR finTransfer --- README.md | 6 +- src/deployer/near.ts | 138 ++++++++++++++++--- src/openapi.json | 320 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 443 insertions(+), 21 deletions(-) create mode 100644 src/openapi.json diff --git a/README.md b/README.md index e432e01..fdec619 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ console.log(`Status: ${status}`); // 'pending' | 'completed' | 'failed' - [ ] Base OmniTransfer interface - [ ] EVM - - [ ] initTransfer + - [x] initTransfer - [ ] finalizeTransfer - [ ] NEAR - [x] initTransfer - - [ ] finalizeTransfer + - [x] finalizeTransfer - [ ] Solana - - [ ] initTransfer + - [x] initTransfer - [ ] finalizeTransfer #### Query Functions diff --git a/src/deployer/near.ts b/src/deployer/near.ts index f54b9e3..f3c0a97 100644 --- a/src/deployer/near.ts +++ b/src/deployer/near.ts @@ -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, @@ -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 @@ -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 /** @@ -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 } @@ -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, }) @@ -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 { + 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 @@ -284,23 +369,38 @@ export class NearDeployer { */ 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, - }, - }), - ]) + 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 @@ -314,6 +414,8 @@ export class NearDeployer { return { regBalance: BigInt(regBalanceStr), initBalance: BigInt(initBalanceStr), + finBalance: BigInt(finBalanceStr), + bindBalance: BigInt(bindBalanceStr), storage: convertedStorage, } } catch (error) { diff --git a/src/openapi.json b/src/openapi.json new file mode 100644 index 0000000..fcf1877 --- /dev/null +++ b/src/openapi.json @@ -0,0 +1,320 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "omni-indexer-endpoint", + "description": "", + "license": { + "name": "" + }, + "version": "0.1.0" + }, + "paths": { + "/api/v1/transfer-fee": { + "get": { + "operationId": "transfer_fee", + "parameters": [ + { + "name": "sender", + "in": "query", + "description": "Omni address of the sender", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "recipient", + "in": "query", + "description": "Omni address of the recipient", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "token", + "in": "query", + "description": "Near token account id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Fees needed to transfer tokens", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["native_token_fee", "usd_fee"], + "properties": { + "native_token_fee": { + "$ref": "#/components/schemas/NativeToken", + "description": "The amount of fee in the native token" + }, + "transferred_token_fee": { + "type": ["integer", "null"], + "description": "The amount of fee in the transferred token. Set only if available", + "minimum": 0 + }, + "usd_fee": { + "type": "number", + "format": "double", + "description": "The amount of fee in usd." + } + } + } + } + } + } + } + } + }, + "/api/v1/transfers/transfer/": { + "get": { + "operationId": "get_transfer", + "parameters": [ + { + "name": "origin_chain", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "Eth", + "enum": ["Eth", "Near", "Sol", "Arb", "Base"] + } + }, + { + "name": "origin_nonce", + "in": "query", + "description": "The transfer nonce that is maintained on the source chain", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "Omni transfer", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Transfer" + } + } + } + } + } + } + }, + "/api/v1/transfers/transfer/status": { + "get": { + "operationId": "get_transfer_status", + "parameters": [ + { + "name": "origin_chain", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "Eth", + "enum": ["Eth", "Near", "Sol", "Arb", "Base"] + } + }, + { + "name": "origin_nonce", + "in": "query", + "description": "The transfer nonce that is maintained on the source chain", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string", + "enum": ["Initialized", "FinalisedOnNear", "Finalised"] + } + } + } + }, + "404": { + "description": "Transfer missing" + }, + "500": { + "description": "Internal server error" + } + } + } + } + }, + "components": { + "schemas": { + "NativeToken": { + "type": "integer", + "minimum": 0 + }, + "Transaction": { + "oneOf": [ + { + "type": "object", + "required": ["NearReceipt"], + "properties": { + "NearReceipt": { + "type": "object", + "required": ["block_height", "block_timestamp_seconds", "transaction_hash"], + "properties": { + "block_height": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "block_timestamp_seconds": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "transaction_hash": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "required": ["EVMLog"], + "properties": { + "EVMLog": { + "type": "object", + "required": ["block_height", "block_timestamp_seconds", "transaction_hash"], + "properties": { + "block_height": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "block_timestamp_seconds": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "transaction_hash": { + "type": "string" + } + } + } + } + } + ] + }, + "Transfer": { + "type": "object", + "required": ["id", "updated_fee", "transfer_message"], + "properties": { + "finalised": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/Transaction" + } + ] + }, + "finalised_on_near": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/Transaction" + } + ] + }, + "id": { + "type": "object", + "required": ["origin_chain", "origin_nonce"], + "properties": { + "origin_chain": { + "type": "string", + "default": "Eth", + "enum": ["Eth", "Near", "Sol", "Arb", "Base"] + }, + "origin_nonce": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + }, + "initialized": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/Transaction" + } + ] + }, + "transfer_message": { + "type": "object", + "required": ["token", "amount", "sender", "recipient", "fee", "msg"], + "properties": { + "amount": { + "type": "integer", + "minimum": 0 + }, + "fee": { + "type": "object", + "required": ["fee", "native_fee"], + "properties": { + "fee": { + "type": "integer", + "minimum": 0 + }, + "native_fee": { + "type": "integer", + "minimum": 0 + } + } + }, + "msg": { + "type": "string" + }, + "recipient": { + "type": "string" + }, + "sender": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "updated_fee": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transaction" + }, + "description": "Up to 10 of the update fee transactions." + } + } + } + } + } +} From e9e11b89402b48a7beb64e0d8de45df7952650f8 Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:05:13 -0800 Subject: [PATCH 2/5] Remove this --- src/openapi.json | 320 ----------------------------------------------- 1 file changed, 320 deletions(-) delete mode 100644 src/openapi.json diff --git a/src/openapi.json b/src/openapi.json deleted file mode 100644 index fcf1877..0000000 --- a/src/openapi.json +++ /dev/null @@ -1,320 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "omni-indexer-endpoint", - "description": "", - "license": { - "name": "" - }, - "version": "0.1.0" - }, - "paths": { - "/api/v1/transfer-fee": { - "get": { - "operationId": "transfer_fee", - "parameters": [ - { - "name": "sender", - "in": "query", - "description": "Omni address of the sender", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "recipient", - "in": "query", - "description": "Omni address of the recipient", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "token", - "in": "query", - "description": "Near token account id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Fees needed to transfer tokens", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["native_token_fee", "usd_fee"], - "properties": { - "native_token_fee": { - "$ref": "#/components/schemas/NativeToken", - "description": "The amount of fee in the native token" - }, - "transferred_token_fee": { - "type": ["integer", "null"], - "description": "The amount of fee in the transferred token. Set only if available", - "minimum": 0 - }, - "usd_fee": { - "type": "number", - "format": "double", - "description": "The amount of fee in usd." - } - } - } - } - } - } - } - } - }, - "/api/v1/transfers/transfer/": { - "get": { - "operationId": "get_transfer", - "parameters": [ - { - "name": "origin_chain", - "in": "query", - "required": false, - "schema": { - "type": "string", - "default": "Eth", - "enum": ["Eth", "Near", "Sol", "Arb", "Base"] - } - }, - { - "name": "origin_nonce", - "in": "query", - "description": "The transfer nonce that is maintained on the source chain", - "required": true, - "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - } - ], - "responses": { - "200": { - "description": "Omni transfer", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Transfer" - } - } - } - } - } - } - }, - "/api/v1/transfers/transfer/status": { - "get": { - "operationId": "get_transfer_status", - "parameters": [ - { - "name": "origin_chain", - "in": "query", - "required": false, - "schema": { - "type": "string", - "default": "Eth", - "enum": ["Eth", "Near", "Sol", "Arb", "Base"] - } - }, - { - "name": "origin_nonce", - "in": "query", - "description": "The transfer nonce that is maintained on the source chain", - "required": true, - "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string", - "enum": ["Initialized", "FinalisedOnNear", "Finalised"] - } - } - } - }, - "404": { - "description": "Transfer missing" - }, - "500": { - "description": "Internal server error" - } - } - } - } - }, - "components": { - "schemas": { - "NativeToken": { - "type": "integer", - "minimum": 0 - }, - "Transaction": { - "oneOf": [ - { - "type": "object", - "required": ["NearReceipt"], - "properties": { - "NearReceipt": { - "type": "object", - "required": ["block_height", "block_timestamp_seconds", "transaction_hash"], - "properties": { - "block_height": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "block_timestamp_seconds": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "transaction_hash": { - "type": "string" - } - } - } - } - }, - { - "type": "object", - "required": ["EVMLog"], - "properties": { - "EVMLog": { - "type": "object", - "required": ["block_height", "block_timestamp_seconds", "transaction_hash"], - "properties": { - "block_height": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "block_timestamp_seconds": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "transaction_hash": { - "type": "string" - } - } - } - } - } - ] - }, - "Transfer": { - "type": "object", - "required": ["id", "updated_fee", "transfer_message"], - "properties": { - "finalised": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/Transaction" - } - ] - }, - "finalised_on_near": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/Transaction" - } - ] - }, - "id": { - "type": "object", - "required": ["origin_chain", "origin_nonce"], - "properties": { - "origin_chain": { - "type": "string", - "default": "Eth", - "enum": ["Eth", "Near", "Sol", "Arb", "Base"] - }, - "origin_nonce": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - } - }, - "initialized": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/Transaction" - } - ] - }, - "transfer_message": { - "type": "object", - "required": ["token", "amount", "sender", "recipient", "fee", "msg"], - "properties": { - "amount": { - "type": "integer", - "minimum": 0 - }, - "fee": { - "type": "object", - "required": ["fee", "native_fee"], - "properties": { - "fee": { - "type": "integer", - "minimum": 0 - }, - "native_fee": { - "type": "integer", - "minimum": 0 - } - } - }, - "msg": { - "type": "string" - }, - "recipient": { - "type": "string" - }, - "sender": { - "type": "string" - }, - "token": { - "type": "string" - } - } - }, - "updated_fee": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Transaction" - }, - "description": "Up to 10 of the update fee transactions." - } - } - } - } - } -} From 497792f6f779695f416228d4ac19b4400dd80fc9 Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:25:07 -0800 Subject: [PATCH 3/5] Fix broken test --- tests/chains/near.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/chains/near.test.ts b/tests/chains/near.test.ts index 0c184fe..29bccec 100644 --- a/tests/chains/near.test.ts +++ b/tests/chains/near.test.ts @@ -102,10 +102,7 @@ describe("NearDeployer", () => { expect(mockWallet.functionCall).toHaveBeenCalledWith({ contractId: mockLockerAddress, methodName: "bind_token", - args: { - chain_kind: destinationChain, - prover_args: expect.any(Uint8Array), - }, + args: Uint8Array.from([1, 2, 3]), gas: BigInt(3e14), attachedDeposit: BigInt(2e23), }) From 76d014d040068497d1475f278839d270ecfe952f Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:28:47 -0800 Subject: [PATCH 4/5] Add tests for near fin transfer --- tests/chains/near.test.ts | 93 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/tests/chains/near.test.ts b/tests/chains/near.test.ts index 29bccec..6c00c9b 100644 --- a/tests/chains/near.test.ts +++ b/tests/chains/near.test.ts @@ -1,7 +1,7 @@ import type { Account } from "near-api-js" import { beforeEach, describe, expect, it, vi } from "vitest" import { NearDeployer } from "../../src/deployer/near" -import { ChainKind } from "../../src/types" +import { ChainKind, ProofKind } from "../../src/types" // Mock the entire borsher module vi.mock("borsher", () => ({ @@ -13,7 +13,6 @@ vi.mock("borsher", () => ({ Struct: vi.fn().mockReturnValue({}), Option: vi.fn().mockReturnValue({}), Vec: vi.fn().mockReturnValue({}), - // Add any other schema types you're using }, })) @@ -109,6 +108,96 @@ describe("NearDeployer", () => { expect(txHash).toBe(mockTxHash) }) }) + describe("finalizeTransfer", () => { + const mockToken = "test-token.near" + const mockAccount = "recipient.near" + const mockStorageDeposit = BigInt(1000000000000000000000000) + const mockVaa = "mock-vaa" + const mockEvmProof = { + proof_kind: ProofKind.FinTransfer, + proof: { + log_index: BigInt(1), + log_entry_data: new Uint8Array([1, 2, 3]), + receipt_index: BigInt(0), + receipt_data: new Uint8Array([4, 5, 6]), + header_data: new Uint8Array([7, 8, 9]), + proof: [new Uint8Array([10, 11, 12])], + }, + } + + it("should throw error if neither VAA nor EVM proof is provided", async () => { + await expect( + deployer.finalizeTransfer(mockToken, mockAccount, mockStorageDeposit, ChainKind.Near), + ).rejects.toThrow("Must provide either VAA or EVM proof") + }) + + it("should throw error if EVM proof is provided for non-EVM chain", async () => { + await expect( + deployer.finalizeTransfer( + mockToken, + mockAccount, + mockStorageDeposit, + ChainKind.Near, + undefined, + mockEvmProof, + ), + ).rejects.toThrow("EVM proof is only valid for Ethereum, Arbitrum, or Base") + }) + + it("should call finalize_transfer with VAA correctly", async () => { + const txHash = await deployer.finalizeTransfer( + mockToken, + mockAccount, + mockStorageDeposit, + ChainKind.Sol, + mockVaa, + ) + + expect(mockWallet.functionCall).toHaveBeenCalledWith({ + contractId: mockLockerAddress, + methodName: "finalize_transfer", + args: expect.any(Uint8Array), + gas: BigInt(3e14), + attachedDeposit: BigInt(1), + }) + expect(txHash).toBe(mockTxHash) + }) + + it("should call finalize_transfer with EVM proof correctly", async () => { + const txHash = await deployer.finalizeTransfer( + mockToken, + mockAccount, + mockStorageDeposit, + ChainKind.Eth, + undefined, + mockEvmProof, + ) + + expect(mockWallet.functionCall).toHaveBeenCalledWith({ + contractId: mockLockerAddress, + methodName: "finalize_transfer", + args: expect.any(Uint8Array), + gas: BigInt(3e14), + attachedDeposit: BigInt(1), + }) + expect(txHash).toBe(mockTxHash) + }) + + it("should handle errors from functionCall", async () => { + const error = new Error("NEAR finalize transfer error") + mockWallet.functionCall = vi.fn().mockRejectedValue(error) + + await expect( + deployer.finalizeTransfer( + mockToken, + mockAccount, + mockStorageDeposit, + ChainKind.Sol, + mockVaa, + ), + ).rejects.toThrow("NEAR finalize transfer error") + }) + }) describe("error handling", () => { it("should propagate errors from functionCall", async () => { From 7ce8a76e2f53d8121f1cc6998faa4a97032eaf0d Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:48:07 -0800 Subject: [PATCH 5/5] Add EVM finTransfer --- README.md | 6 ++--- src/deployer/evm.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++- src/types/common.ts | 1 - src/types/evm.ts | 34 +++++++++++++++++++++++++++ src/types/index.ts | 1 + src/types/prover.ts | 4 ++-- 6 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 src/types/evm.ts diff --git a/README.md b/README.md index fdec619..40be82f 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,10 @@ console.log(`Status: ${status}`); // 'pending' | 'completed' | 'failed' #### Core Transfer Interface - [ ] Base OmniTransfer interface - - [ ] EVM + - [x] EVM - [x] initTransfer - - [ ] finalizeTransfer - - [ ] NEAR + - [x] finalizeTransfer + - [x] NEAR - [x] initTransfer - [x] finalizeTransfer - [ ] Solana diff --git a/src/deployer/evm.ts b/src/deployer/evm.ts index c1673c0..ad7c316 100644 --- a/src/deployer/evm.ts +++ b/src/deployer/evm.ts @@ -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 @@ -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 { + // 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 + } } diff --git a/src/types/common.ts b/src/types/common.ts index 774f903..367e1c6 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -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 = diff --git a/src/types/evm.ts b/src/types/evm.ts new file mode 100644 index 0000000..7431303 --- /dev/null +++ b/src/types/evm.ts @@ -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 +} diff --git a/src/types/index.ts b/src/types/index.ts index fc5af04..4454181 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,4 @@ export * from "./locker" export * from "./omni" export * from "./prover" export * from "./mpc" +export * from "./evm" diff --git a/src/types/prover.ts b/src/types/prover.ts index 8f8e421..3e04396 100644 --- a/src/types/prover.ts +++ b/src/types/prover.ts @@ -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, @@ -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