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
65 changes: 65 additions & 0 deletions packages/devtools-aptos/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "@layerzerolabs/devtools-aptos",
"version": "0.1.0",
"description": "Developer utilities for working with LayerZero Aptos contracts",
"repository": {
"type": "git",
"url": "git+https://github.com/LayerZero-Labs/devtools.git",
"directory": "packages/devtools-aptos"
},
"license": "MIT",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./*": {
"types": "./dist/*.d.ts",
"require": "./dist/*.js",
"import": "./dist/*.mjs"
}
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"./dist/index.*"
],
"scripts": {
"prebuild": "tsc -noEmit",
"build": "$npm_execpath tsup --clean",
"clean": "rm -rf dist",
"dev": "$npm_execpath tsup --watch",
"lint": "$npm_execpath eslint '**/*.{js,ts,json}'",
"lint:fix": "eslint --fix '**/*.{js,ts,json}'",
"test": "jest --ci --passWithNoTests"
},
"dependencies": {
"p-memoize": "~4.0.4"
},
"devDependencies": {
"@aptos-labs/ts-sdk": "^1.33.1",
"@layerzerolabs/devtools": "~2.0.4",
"@layerzerolabs/io-devtools": "~0.3.2",
"@layerzerolabs/lz-aptos-sdk-v2": "^3.0.156",
"@layerzerolabs/lz-definitions": "^3.0.148",
"@swc/core": "^1.4.0",
"@swc/jest": "^0.2.36",
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-node": "^10.9.2",
"tslib": "~2.6.2",
"tsup": "~8.0.1",
"typescript": "^5.4.4"
},
"peerDependencies": {
"@aptos-labs/ts-sdk": "^1.33.1",
"@layerzerolabs/devtools": "~2.0.4",
"@layerzerolabs/io-devtools": "~0.3.2",
"@layerzerolabs/lz-definitions": "^3.0.148"
},
"publishConfig": {
"access": "public"
}
}
79 changes: 79 additions & 0 deletions packages/devtools-aptos/src/common/addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { OmniAddress, Bytes32 } from '@layerzerolabs/devtools'

/**
* Converts a hexadecimal address string to a 32-byte Uint8Array format used by Aptos
*
* @param address - The hex address string to convert, optionally starting with '0x'. Can be null/undefined.
* @returns A 32-byte Uint8Array with the address right-aligned (padded with zeros on the left)
*
* If the input is null/undefined, returns an empty 32-byte array.
* Otherwise, removes '0x' prefix if present, converts hex string to bytes,
* and right-aligns the result in a 32-byte array.
*/
export function hexAddrToAptosBytesAddr(address: string | null | undefined): Uint8Array {
const bytes = address ? Buffer.from(address.replace('0x', ''), 'hex') : new Uint8Array(0)
const bytes32 = new Uint8Array(32)
bytes32.set(bytes, 32 - bytes.length)
return bytes32
}

/**
* Converts a Uint8Array to a hex string with 0x prefix
*
* @param bytes - The bytes to convert
* @returns Hex string with 0x prefix
*/
export function bytesToHex(bytes: Uint8Array): string {
return '0x' + Buffer.from(bytes).toString('hex')
}

/**
* Normalizes an address to bytes32 format (64 hex characters with 0x prefix)
*
* Aptos uses 32-byte addresses. This function ensures addresses from
* other chains (like EVM with 20 bytes) are properly padded to 32 bytes.
*
* @param address - The address to normalize
* @returns Normalized bytes32 address
*/
export function normalizeAddressToBytes32(address: OmniAddress | null | undefined): Bytes32 {
if (!address) {
return '0x' + '0'.repeat(64)
}

// Remove 0x prefix if present
const hex = address.replace('0x', '')

// Pad to 64 characters (32 bytes)
const padded = hex.padStart(64, '0')

return `0x${padded}`
}

/**
* Checks if an address is an empty/zero address
*
* @param address - The address to check
* @returns true if the address is null, undefined, or all zeros
*/
export function isEmptyAddress(address: OmniAddress | null | undefined): boolean {
if (!address) {
return true
}

const normalized = normalizeAddressToBytes32(address)
return normalized === '0x' + '0'.repeat(64)
}

/**
* Compares two addresses for equality, handling different lengths
*
* Both addresses are normalized to bytes32 before comparison.
*
* @param a - First address
* @param b - Second address
* @returns true if addresses are equal
*/
export function areAddressesEqual(a: OmniAddress | null | undefined, b: OmniAddress | null | undefined): boolean {
return normalizeAddressToBytes32(a) === normalizeAddressToBytes32(b)
}
80 changes: 80 additions & 0 deletions packages/devtools-aptos/src/connection/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk'
import { EndpointId, getNetworkForChainId, Stage } from '@layerzerolabs/lz-definitions'

import type { ConnectionFactory, RpcUrlFactory } from './types'

/**
* Default RPC URLs for Aptos networks
*/
const DEFAULT_RPC_URLS: Partial<Record<EndpointId, string>> = {
[EndpointId.APTOS_V2_MAINNET]: 'https://fullnode.mainnet.aptoslabs.com/v1',
[EndpointId.APTOS_V2_TESTNET]: 'https://fullnode.testnet.aptoslabs.com/v1',
}

/**
* Creates a factory that returns RPC URLs based on endpoint ID
*
* The factory will first check for environment variables in the format:
* - RPC_URL_APTOS (for mainnet)
* - RPC_URL_APTOS_TESTNET (for testnet)
*
* If no environment variable is set, it falls back to the default public RPC URLs
*/
export const createRpcUrlFactory = (): RpcUrlFactory => {
return async (eid: EndpointId): Promise<string> => {
const network = getNetworkForChainId(eid)

// Check for environment variable
const envVarSuffix = network.env === Stage.MAINNET ? '' : `_${network.env.toUpperCase()}`
const envVar = `RPC_URL_APTOS${envVarSuffix}`
const envUrl = process.env[envVar]

if (envUrl) {
return envUrl
}

// Fall back to default
const defaultUrl = DEFAULT_RPC_URLS[eid]
if (defaultUrl) {
return defaultUrl
}

throw new Error(`No RPC URL configured for Aptos endpoint ${eid}. Set ${envVar} environment variable.`)
}
}

/**
* Creates a factory that returns Aptos client connections based on endpoint ID
*
* @param urlFactory - Optional factory for RPC URLs. Defaults to createRpcUrlFactory()
* @returns ConnectionFactory for Aptos clients
*/
export const createConnectionFactory = (urlFactory: RpcUrlFactory = createRpcUrlFactory()): ConnectionFactory => {
// Cache connections by endpoint ID to avoid creating multiple clients
const connections = new Map<EndpointId, Aptos>()

return async (eid: EndpointId): Promise<Aptos> => {
// Return cached connection if available
const cached = connections.get(eid)
if (cached) {
return cached
}

// Get the RPC URL
const url = await urlFactory(eid)

// Create the Aptos config and client
const config = new AptosConfig({
fullnode: url,
// Use custom network since we're providing a custom URL
network: Network.CUSTOM,
})

const aptos = new Aptos(config)

// Cache the connection
connections.set(eid, aptos)

return aptos
}
}
12 changes: 12 additions & 0 deletions packages/devtools-aptos/src/connection/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Aptos } from '@aptos-labs/ts-sdk'
import type { EndpointId } from '@layerzerolabs/lz-definitions'

/**
* Factory function that creates Aptos client connections based on endpoint ID
*/
export type ConnectionFactory = (eid: EndpointId) => Promise<Aptos>

/**
* Factory function that returns RPC URLs based on endpoint ID
*/
export type RpcUrlFactory = (eid: EndpointId) => Promise<string>
13 changes: 13 additions & 0 deletions packages/devtools-aptos/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Connection
export * from './connection/factory'
export * from './connection/types'

// Transactions
export * from './transactions/signer'
export * from './transactions/types'

// OmniSDK
export * from './omnigraph/sdk'

// Common utilities
export * from './common/addresses'
39 changes: 39 additions & 0 deletions packages/devtools-aptos/src/omnigraph/sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Aptos } from '@aptos-labs/ts-sdk'
import type { OmniPoint, IOmniSDK } from '@layerzerolabs/devtools'

import type { ConnectionFactory } from '../connection/types'

/**
* Base OmniSDK implementation for Aptos
*
* This provides the foundation for building Aptos-specific SDKs
* that integrate with the OmniGraph framework.
*/
export abstract class OmniSDK implements IOmniSDK {
public readonly point: OmniPoint

protected aptos?: Aptos

constructor(
point: OmniPoint,
protected readonly connectionFactory?: ConnectionFactory
) {
this.point = point
}

/**
* Get or create the Aptos client connection
*/
protected async getAptos(): Promise<Aptos> {
if (this.aptos) {
return this.aptos
}

if (!this.connectionFactory) {
throw new Error('ConnectionFactory is required to create Aptos client')
}

this.aptos = await this.connectionFactory(this.point.eid)
return this.aptos
}
}
Loading