Skip to content

Commit 9fd740c

Browse files
committed
feat(devtools-aptos): add Aptos devtools packages for OmniGraph support
Add three new packages for Aptos devtools: - @layerzerolabs/devtools-aptos: Core Aptos utilities (connection, signer, OmniSDK) - @layerzerolabs/protocol-devtools-aptos: EndpointV2 and ULN302 SDKs - @layerzerolabs/ua-devtools-aptos: OFT SDK implementing IOApp interface These packages enable lz:oapp:wire support for Aptos OFTs following the OmniGraph pattern used by Sui and Starknet.
1 parent 99d2d96 commit 9fd740c

24 files changed

Lines changed: 2227 additions & 165 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"name": "@layerzerolabs/devtools-aptos",
3+
"version": "0.1.0",
4+
"description": "Developer utilities for working with LayerZero Aptos contracts",
5+
"repository": {
6+
"type": "git",
7+
"url": "git+https://github.com/LayerZero-Labs/devtools.git",
8+
"directory": "packages/devtools-aptos"
9+
},
10+
"license": "MIT",
11+
"exports": {
12+
".": {
13+
"types": "./dist/index.d.ts",
14+
"require": "./dist/index.js",
15+
"import": "./dist/index.mjs"
16+
},
17+
"./*": {
18+
"types": "./dist/*.d.ts",
19+
"require": "./dist/*.js",
20+
"import": "./dist/*.mjs"
21+
}
22+
},
23+
"main": "./dist/index.js",
24+
"module": "./dist/index.mjs",
25+
"types": "./dist/index.d.ts",
26+
"files": [
27+
"./dist/index.*"
28+
],
29+
"scripts": {
30+
"prebuild": "tsc -noEmit",
31+
"build": "$npm_execpath tsup --clean",
32+
"clean": "rm -rf dist",
33+
"dev": "$npm_execpath tsup --watch",
34+
"lint": "$npm_execpath eslint '**/*.{js,ts,json}'",
35+
"lint:fix": "eslint --fix '**/*.{js,ts,json}'",
36+
"test": "jest --ci --passWithNoTests"
37+
},
38+
"dependencies": {
39+
"p-memoize": "~4.0.4"
40+
},
41+
"devDependencies": {
42+
"@aptos-labs/ts-sdk": "^1.33.1",
43+
"@layerzerolabs/devtools": "~2.0.4",
44+
"@layerzerolabs/io-devtools": "~0.3.2",
45+
"@layerzerolabs/lz-aptos-sdk-v2": "^3.0.156",
46+
"@layerzerolabs/lz-definitions": "^3.0.148",
47+
"@swc/core": "^1.4.0",
48+
"@swc/jest": "^0.2.36",
49+
"@types/jest": "^29.5.12",
50+
"jest": "^29.7.0",
51+
"ts-node": "^10.9.2",
52+
"tslib": "~2.6.2",
53+
"tsup": "~8.0.1",
54+
"typescript": "^5.4.4"
55+
},
56+
"peerDependencies": {
57+
"@aptos-labs/ts-sdk": "^1.33.1",
58+
"@layerzerolabs/devtools": "~2.0.4",
59+
"@layerzerolabs/io-devtools": "~0.3.2",
60+
"@layerzerolabs/lz-definitions": "^3.0.148"
61+
},
62+
"publishConfig": {
63+
"access": "public"
64+
}
65+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { OmniAddress, Bytes32 } from '@layerzerolabs/devtools'
2+
3+
/**
4+
* Converts a hexadecimal address string to a 32-byte Uint8Array format used by Aptos
5+
*
6+
* @param address - The hex address string to convert, optionally starting with '0x'. Can be null/undefined.
7+
* @returns A 32-byte Uint8Array with the address right-aligned (padded with zeros on the left)
8+
*
9+
* If the input is null/undefined, returns an empty 32-byte array.
10+
* Otherwise, removes '0x' prefix if present, converts hex string to bytes,
11+
* and right-aligns the result in a 32-byte array.
12+
*/
13+
export function hexAddrToAptosBytesAddr(address: string | null | undefined): Uint8Array {
14+
const bytes = address ? Buffer.from(address.replace('0x', ''), 'hex') : new Uint8Array(0)
15+
const bytes32 = new Uint8Array(32)
16+
bytes32.set(bytes, 32 - bytes.length)
17+
return bytes32
18+
}
19+
20+
/**
21+
* Converts a Uint8Array to a hex string with 0x prefix
22+
*
23+
* @param bytes - The bytes to convert
24+
* @returns Hex string with 0x prefix
25+
*/
26+
export function bytesToHex(bytes: Uint8Array): string {
27+
return '0x' + Buffer.from(bytes).toString('hex')
28+
}
29+
30+
/**
31+
* Normalizes an address to bytes32 format (64 hex characters with 0x prefix)
32+
*
33+
* Aptos uses 32-byte addresses. This function ensures addresses from
34+
* other chains (like EVM with 20 bytes) are properly padded to 32 bytes.
35+
*
36+
* @param address - The address to normalize
37+
* @returns Normalized bytes32 address
38+
*/
39+
export function normalizeAddressToBytes32(address: OmniAddress | null | undefined): Bytes32 {
40+
if (!address) {
41+
return '0x' + '0'.repeat(64)
42+
}
43+
44+
// Remove 0x prefix if present
45+
const hex = address.replace('0x', '')
46+
47+
// Pad to 64 characters (32 bytes)
48+
const padded = hex.padStart(64, '0')
49+
50+
return `0x${padded}`
51+
}
52+
53+
/**
54+
* Checks if an address is an empty/zero address
55+
*
56+
* @param address - The address to check
57+
* @returns true if the address is null, undefined, or all zeros
58+
*/
59+
export function isEmptyAddress(address: OmniAddress | null | undefined): boolean {
60+
if (!address) {
61+
return true
62+
}
63+
64+
const normalized = normalizeAddressToBytes32(address)
65+
return normalized === '0x' + '0'.repeat(64)
66+
}
67+
68+
/**
69+
* Compares two addresses for equality, handling different lengths
70+
*
71+
* Both addresses are normalized to bytes32 before comparison.
72+
*
73+
* @param a - First address
74+
* @param b - Second address
75+
* @returns true if addresses are equal
76+
*/
77+
export function areAddressesEqual(a: OmniAddress | null | undefined, b: OmniAddress | null | undefined): boolean {
78+
return normalizeAddressToBytes32(a) === normalizeAddressToBytes32(b)
79+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk'
2+
import { EndpointId, getNetworkForChainId, Stage } from '@layerzerolabs/lz-definitions'
3+
4+
import type { ConnectionFactory, RpcUrlFactory } from './types'
5+
6+
/**
7+
* Default RPC URLs for Aptos networks
8+
*/
9+
const DEFAULT_RPC_URLS: Partial<Record<EndpointId, string>> = {
10+
[EndpointId.APTOS_V2_MAINNET]: 'https://fullnode.mainnet.aptoslabs.com/v1',
11+
[EndpointId.APTOS_V2_TESTNET]: 'https://fullnode.testnet.aptoslabs.com/v1',
12+
}
13+
14+
/**
15+
* Creates a factory that returns RPC URLs based on endpoint ID
16+
*
17+
* The factory will first check for environment variables in the format:
18+
* - RPC_URL_APTOS (for mainnet)
19+
* - RPC_URL_APTOS_TESTNET (for testnet)
20+
*
21+
* If no environment variable is set, it falls back to the default public RPC URLs
22+
*/
23+
export const createRpcUrlFactory = (): RpcUrlFactory => {
24+
return async (eid: EndpointId): Promise<string> => {
25+
const network = getNetworkForChainId(eid)
26+
27+
// Check for environment variable
28+
const envVarSuffix = network.env === Stage.MAINNET ? '' : `_${network.env.toUpperCase()}`
29+
const envVar = `RPC_URL_APTOS${envVarSuffix}`
30+
const envUrl = process.env[envVar]
31+
32+
if (envUrl) {
33+
return envUrl
34+
}
35+
36+
// Fall back to default
37+
const defaultUrl = DEFAULT_RPC_URLS[eid]
38+
if (defaultUrl) {
39+
return defaultUrl
40+
}
41+
42+
throw new Error(`No RPC URL configured for Aptos endpoint ${eid}. Set ${envVar} environment variable.`)
43+
}
44+
}
45+
46+
/**
47+
* Creates a factory that returns Aptos client connections based on endpoint ID
48+
*
49+
* @param urlFactory - Optional factory for RPC URLs. Defaults to createRpcUrlFactory()
50+
* @returns ConnectionFactory for Aptos clients
51+
*/
52+
export const createConnectionFactory = (urlFactory: RpcUrlFactory = createRpcUrlFactory()): ConnectionFactory => {
53+
// Cache connections by endpoint ID to avoid creating multiple clients
54+
const connections = new Map<EndpointId, Aptos>()
55+
56+
return async (eid: EndpointId): Promise<Aptos> => {
57+
// Return cached connection if available
58+
const cached = connections.get(eid)
59+
if (cached) {
60+
return cached
61+
}
62+
63+
// Get the RPC URL
64+
const url = await urlFactory(eid)
65+
66+
// Create the Aptos config and client
67+
const config = new AptosConfig({
68+
fullnode: url,
69+
// Use custom network since we're providing a custom URL
70+
network: Network.CUSTOM,
71+
})
72+
73+
const aptos = new Aptos(config)
74+
75+
// Cache the connection
76+
connections.set(eid, aptos)
77+
78+
return aptos
79+
}
80+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Aptos } from '@aptos-labs/ts-sdk'
2+
import type { EndpointId } from '@layerzerolabs/lz-definitions'
3+
4+
/**
5+
* Factory function that creates Aptos client connections based on endpoint ID
6+
*/
7+
export type ConnectionFactory = (eid: EndpointId) => Promise<Aptos>
8+
9+
/**
10+
* Factory function that returns RPC URLs based on endpoint ID
11+
*/
12+
export type RpcUrlFactory = (eid: EndpointId) => Promise<string>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Connection
2+
export * from './connection/factory'
3+
export * from './connection/types'
4+
5+
// Transactions
6+
export * from './transactions/signer'
7+
export * from './transactions/types'
8+
9+
// OmniSDK
10+
export * from './omnigraph/sdk'
11+
12+
// Common utilities
13+
export * from './common/addresses'
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Aptos } from '@aptos-labs/ts-sdk'
2+
import type { OmniPoint, IOmniSDK } from '@layerzerolabs/devtools'
3+
4+
import type { ConnectionFactory } from '../connection/types'
5+
6+
/**
7+
* Base OmniSDK implementation for Aptos
8+
*
9+
* This provides the foundation for building Aptos-specific SDKs
10+
* that integrate with the OmniGraph framework.
11+
*/
12+
export abstract class OmniSDK implements IOmniSDK {
13+
public readonly point: OmniPoint
14+
15+
protected aptos?: Aptos
16+
17+
constructor(
18+
point: OmniPoint,
19+
protected readonly connectionFactory?: ConnectionFactory
20+
) {
21+
this.point = point
22+
}
23+
24+
/**
25+
* Get or create the Aptos client connection
26+
*/
27+
protected async getAptos(): Promise<Aptos> {
28+
if (this.aptos) {
29+
return this.aptos
30+
}
31+
32+
if (!this.connectionFactory) {
33+
throw new Error('ConnectionFactory is required to create Aptos client')
34+
}
35+
36+
this.aptos = await this.connectionFactory(this.point.eid)
37+
return this.aptos
38+
}
39+
}

0 commit comments

Comments
 (0)