Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The library provides chain adapters for the following blockchain networks:
- **Aptos**: Move-based blockchain with Ed25519 signature support
- **SUI**: Move-based blockchain with Ed25519 signature support
- **XRP Ledger**: XRP mainnet, testnet, and devnet with native XRP transfers
- **MultiversX**: Formerly Elrond network with Ed25519 accounts, native EGLD transfers, and API provider support

Each chain adapter provides a unified interface for:
- Address and public key derivation
Expand Down
163 changes: 163 additions & 0 deletions __tests__/multiversx/MultiversX.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
import bs58 from 'bs58'

import {
Address,
AccountOnNetwork,
NetworkConfig,
type INetworkProvider,
type NetworkEntrypoint,
} from '@multiversx/sdk-core'
import type { MockedFunction } from 'jest-mock'

import { MultiversX } from '../../src/chain-adapters/MultiversX/MultiversX'
import type { MultiversXTransactionRequest } from '../../src/chain-adapters/MultiversX/types'
import type { ChainSignatureContract } from '../../src/contracts/ChainSignatureContract'

type ProviderSubset = Pick<
INetworkProvider,
'getNetworkConfig' | 'getAccount' | 'sendTransaction'
>

describe('MultiversX Chain Adapter', () => {
const basePublicKey = Buffer.alloc(32, 1)
const derivedPublicKey = `ed25519:${bs58.encode(basePublicKey)}`
const sampleSenderAddress = new Address(basePublicKey).toBech32()
const sampleReceiverAddress = new Address(Buffer.alloc(32, 2)).toBech32()

let networkConfig: NetworkConfig
let mockProvider: {
getNetworkConfig: MockedFunction<ProviderSubset['getNetworkConfig']>
getAccount: MockedFunction<ProviderSubset['getAccount']>
sendTransaction: MockedFunction<ProviderSubset['sendTransaction']>
}
let getNetworkConfigMock: MockedFunction<ProviderSubset['getNetworkConfig']>
let getAccountMock: MockedFunction<ProviderSubset['getAccount']>
let sendTransactionMock: MockedFunction<ProviderSubset['sendTransaction']>
let getDerivedPublicKeyMock: MockedFunction<(args: any) => Promise<string>>
let mockEntrypoint: NetworkEntrypoint
let mockContract: ChainSignatureContract
let adapter: MultiversX

beforeEach(() => {
networkConfig = new NetworkConfig()
networkConfig.chainID = 'D'
networkConfig.minGasLimit = 50000n
networkConfig.minGasPrice = 1_000_000_000n

const account = new AccountOnNetwork({
address: Address.newFromBech32(sampleSenderAddress),
balance: 1_000_000_000_000_000_000n,
nonce: 7n,
})

getNetworkConfigMock = jest.fn(async () => networkConfig)
getAccountMock = jest.fn(async () => account)
sendTransactionMock = jest.fn(async () => 'mock-hash')

mockProvider = {
getNetworkConfig: getNetworkConfigMock,
getAccount: getAccountMock,
sendTransaction: sendTransactionMock,
}

const createNetworkProvider = jest.fn(
() => mockProvider as unknown as INetworkProvider
)
mockEntrypoint = {
createNetworkProvider,
} as unknown as NetworkEntrypoint

getDerivedPublicKeyMock = jest.fn(async () => derivedPublicKey)

mockContract = {
getDerivedPublicKey: getDerivedPublicKeyMock as unknown as ChainSignatureContract['getDerivedPublicKey'],
sign: jest.fn(),
getPublicKey: jest.fn(),
getCurrentSignatureDeposit: jest.fn().mockReturnValue(1),
} as unknown as ChainSignatureContract

adapter = new MultiversX({
contract: mockContract,
networkEntrypoint: mockEntrypoint,
})
})

it('derives MultiversX address and public key', async () => {
const result = await adapter.deriveAddressAndPublicKey('test.near', 'path')

expect(result.address.startsWith('erd')).toBe(true)
expect(result.publicKey).toBe(`0x${Buffer.from(basePublicKey).toString('hex')}`)
expect(getDerivedPublicKeyMock).toHaveBeenCalled()
})

it('returns balance with EGLD decimals', async () => {
const { balance, decimals } = await adapter.getBalance(sampleSenderAddress)

expect(balance).toBe(BigInt('1000000000000000000'))
expect(decimals).toBe(18)
})

it('handles missing account when fetching balance', async () => {
getAccountMock.mockRejectedValueOnce(new Error('account not found'))

const { balance } = await adapter.getBalance(sampleReceiverAddress)
expect(balance).toBe(0n)
})

it('prepares transaction for signing with network defaults', async () => {
const request: MultiversXTransactionRequest = {
sender: sampleSenderAddress,
receiver: sampleReceiverAddress,
value: '100000000000000000',
data: 'hello',
}

const { transaction, hashesToSign } = await adapter.prepareTransactionForSigning(
request
)

expect(transaction.nonce).toBe(7n)
expect(transaction.gasPrice).toBe(networkConfig.minGasPrice)
expect(transaction.gasLimit).toBe(networkConfig.minGasLimit)
expect(transaction.chainID).toBe(networkConfig.chainID)
expect(Array.isArray(hashesToSign)).toBe(true)
expect(hashesToSign[0].length).toBeGreaterThan(0)
expect(getNetworkConfigMock).toHaveBeenCalledTimes(1)
})

it('serializes and deserializes transactions', async () => {
const request: MultiversXTransactionRequest = {
sender: sampleSenderAddress,
receiver: sampleReceiverAddress,
value: '10',
}

const { transaction } = await adapter.prepareTransactionForSigning(request)
const serialized = adapter.serializeTransaction(transaction)
const restored = adapter.deserializeTransaction(serialized)

expect(restored.toPlainObject()).toEqual(transaction.toPlainObject())
})

it('finalizes signing and broadcasts transaction', async () => {
const request: MultiversXTransactionRequest = {
sender: sampleSenderAddress,
receiver: sampleReceiverAddress,
value: '10',
}

const { transaction } = await adapter.prepareTransactionForSigning(request)
const signed = adapter.finalizeTransactionSigning({
transaction,
rsvSignatures: {
scheme: 'ed25519',
signature: Array.from(Buffer.alloc(64, 2)),
},
})

const result = await adapter.broadcastTx(signed)
expect(result).toEqual({ hash: 'mock-hash' })
expect(sendTransactionMock).toHaveBeenCalled()
})
})
84 changes: 84 additions & 0 deletions examples/send-multiversx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Account } from '@near-js/accounts'
import { type KeyPairString, KeyPair } from '@near-js/crypto'
import { JsonRpcProvider } from '@near-js/providers'
import { KeyPairSigner } from '@near-js/signers'
import { chainAdapters, contracts } from '../src'
import { config } from 'dotenv'
import { DevnetEntrypoint } from '@multiversx/sdk-core/out'

config()

async function main(): Promise<void> {
const accountId = process.env.ACCOUNT_ID
const privateKey = process.env.PRIVATE_KEY as KeyPairString
const receiver = process.env.MULTIVERSX_RECEIVER ?? "erd1fmd662htrgt07xxd8me09newa9s0euzvpz3wp0c4pz78f83grt9qm6pn57"

if (!accountId || !privateKey || !receiver) {
throw new Error(
'ACCOUNT_ID, PRIVATE_KEY and MULTIVERSX_RECEIVER must be provided in environment variables'
)
}

const keyPair = KeyPair.fromString(privateKey)
const signer = new KeyPairSigner(keyPair)
const provider = new JsonRpcProvider({
url: process.env.NEAR_RPC_URL ?? 'https://test.rpc.fastnear.com',
})

const account = new Account(accountId, provider, signer)

const contract = new contracts.ChainSignatureContract({
networkId:
(process.env.NEAR_NETWORK_ID as 'testnet' | 'mainnet') ?? 'testnet',
contractId:
process.env.NEXT_PUBLIC_NEAR_CHAIN_SIGNATURE_CONTRACT ??
'v1.signer-prod.testnet',
})

const mvxChain = new chainAdapters.multiversx.MultiversX({
contract,
networkEntrypoint: new DevnetEntrypoint(),
})

const derivationPath = process.env.MULTIVERSX_DERIVATION_PATH ?? 'mvx-1'

const { address } = await mvxChain.deriveAddressAndPublicKey(
accountId,
derivationPath
)

console.log('Derived MultiversX address:', address)

const { balance } = await mvxChain.getBalance(address)
console.log('Current balance (attoEGLD):', balance.toString())

const request: chainAdapters.multiversx.MultiversXTransactionRequest = {
sender: address,
receiver,
value: process.env.MULTIVERSX_VALUE ?? '10000000000000000', // 0.01 EGLD
}

const { transaction, hashesToSign } =
await mvxChain.prepareTransactionForSigning(request)

const signature = await contract.sign({
payloads: hashesToSign,
path: derivationPath,
keyType: 'Eddsa',
signerAccount: account,
})

const signedTx = mvxChain.finalizeTransactionSigning({
transaction,
rsvSignatures: signature,
})

const { hash } = await mvxChain.broadcastTx(signedTx)

console.log('Submitted MultiversX transaction hash:', hash)
}

main().catch((error) => {
console.error(error)
process.exit(1)
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"@cosmjs/math": "^0.32.4",
"@cosmjs/proto-signing": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@multiversx/sdk-core": "^15.3.0",
"@mysten/sui": "^1.30.1",
"@near-js/accounts": "^2.2.4",
"@near-js/crypto": "^2.2.4",
Expand Down
Loading