From 7f923e93472f510ffb768ef85f72095859ff4ed2 Mon Sep 17 00:00:00 2001 From: Krak Date: Mon, 26 Jan 2026 09:37:41 -0800 Subject: [PATCH 1/8] feat(devtools-sui): add Sui signer and connection factory - Add SuiSigner for transaction signing with sender context - Add createConnectionFactory and createRpcUrlFactory for RPC connections - Add OmniSDK base class with transaction serialization - Add Sui chain type to normalizePeer/denormalizePeer in devtools Key design decisions: - Transaction.serialize() used instead of build() to defer sender context - Signer reconstructs transaction and sets sender during signing - Connection factory reads from RPC_URL_SUI environment variable --- packages/devtools-sui/.eslintignore | 2 + packages/devtools-sui/.eslintrc.json | 3 + packages/devtools-sui/.prettierignore | 2 + packages/devtools-sui/CHANGELOG.md | 5 ++ packages/devtools-sui/README.md | 9 +++ packages/devtools-sui/package.json | 68 ++++++++++++++++++ packages/devtools-sui/src/common/addresses.ts | 9 +++ packages/devtools-sui/src/common/index.ts | 1 + .../devtools-sui/src/connection/factory.ts | 17 +++++ packages/devtools-sui/src/connection/index.ts | 2 + packages/devtools-sui/src/connection/types.ts | 4 ++ packages/devtools-sui/src/index.ts | 4 ++ .../devtools-sui/src/omnigraph/coordinates.ts | 10 +++ packages/devtools-sui/src/omnigraph/index.ts | 3 + packages/devtools-sui/src/omnigraph/sdk.ts | 29 ++++++++ packages/devtools-sui/src/omnigraph/types.ts | 3 + .../devtools-sui/src/transactions/index.ts | 2 + .../devtools-sui/src/transactions/serde.ts | 5 ++ .../devtools-sui/src/transactions/signer.ts | 70 +++++++++++++++++++ packages/devtools-sui/tsconfig.json | 8 +++ packages/devtools-sui/tsup.config.ts | 9 +++ packages/devtools/src/common/bytes.ts | 2 + 22 files changed, 267 insertions(+) create mode 100644 packages/devtools-sui/.eslintignore create mode 100644 packages/devtools-sui/.eslintrc.json create mode 100644 packages/devtools-sui/.prettierignore create mode 100644 packages/devtools-sui/CHANGELOG.md create mode 100644 packages/devtools-sui/README.md create mode 100644 packages/devtools-sui/package.json create mode 100644 packages/devtools-sui/src/common/addresses.ts create mode 100644 packages/devtools-sui/src/common/index.ts create mode 100644 packages/devtools-sui/src/connection/factory.ts create mode 100644 packages/devtools-sui/src/connection/index.ts create mode 100644 packages/devtools-sui/src/connection/types.ts create mode 100644 packages/devtools-sui/src/index.ts create mode 100644 packages/devtools-sui/src/omnigraph/coordinates.ts create mode 100644 packages/devtools-sui/src/omnigraph/index.ts create mode 100644 packages/devtools-sui/src/omnigraph/sdk.ts create mode 100644 packages/devtools-sui/src/omnigraph/types.ts create mode 100644 packages/devtools-sui/src/transactions/index.ts create mode 100644 packages/devtools-sui/src/transactions/serde.ts create mode 100644 packages/devtools-sui/src/transactions/signer.ts create mode 100644 packages/devtools-sui/tsconfig.json create mode 100644 packages/devtools-sui/tsup.config.ts diff --git a/packages/devtools-sui/.eslintignore b/packages/devtools-sui/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/packages/devtools-sui/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/devtools-sui/.eslintrc.json b/packages/devtools-sui/.eslintrc.json new file mode 100644 index 0000000000..be97c53fbb --- /dev/null +++ b/packages/devtools-sui/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/devtools-sui/.prettierignore b/packages/devtools-sui/.prettierignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/packages/devtools-sui/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/packages/devtools-sui/CHANGELOG.md b/packages/devtools-sui/CHANGELOG.md new file mode 100644 index 0000000000..ac0798a1f1 --- /dev/null +++ b/packages/devtools-sui/CHANGELOG.md @@ -0,0 +1,5 @@ +# @layerzerolabs/devtools-sui + +## 0.1.0 + +- Initial release. diff --git a/packages/devtools-sui/README.md b/packages/devtools-sui/README.md new file mode 100644 index 0000000000..ee7f79e6ed --- /dev/null +++ b/packages/devtools-sui/README.md @@ -0,0 +1,9 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/devtools-sui

+ +Utilities for working with LayerZero Sui contracts. diff --git a/packages/devtools-sui/package.json b/packages/devtools-sui/package.json new file mode 100644 index 0000000000..b71a9b4e49 --- /dev/null +++ b/packages/devtools-sui/package.json @@ -0,0 +1,68 @@ +{ + "name": "@layerzerolabs/devtools-sui", + "version": "0.1.0", + "description": "Developer utilities for working with LayerZero Sui contracts", + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/devtools.git", + "directory": "packages/devtools-sui" + }, + "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": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-sui-oft-sdk-v2": "^3.0.156", + "@layerzerolabs/lz-sui-sdk-v2": "^3.0.156", + "@mysten/bcs": "^1.9.2", + "@mysten/sui": "^1.45.2", + "@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": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@mysten/bcs": "^1.9.2", + "@mysten/sui": "^1.45.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/devtools-sui/src/common/addresses.ts b/packages/devtools-sui/src/common/addresses.ts new file mode 100644 index 0000000000..32a5bf72b4 --- /dev/null +++ b/packages/devtools-sui/src/common/addresses.ts @@ -0,0 +1,9 @@ +const SUI_HEX_REGEX = /^0x[0-9a-fA-F]{1,64}$/ + +export const isSuiAddress = (value: string): boolean => SUI_HEX_REGEX.test(value) + +export const assertSuiAddress = (value: string, label = 'address'): void => { + if (!isSuiAddress(value)) { + throw new Error(`Invalid Sui ${label}: ${value}`) + } +} diff --git a/packages/devtools-sui/src/common/index.ts b/packages/devtools-sui/src/common/index.ts new file mode 100644 index 0000000000..897b610db2 --- /dev/null +++ b/packages/devtools-sui/src/common/index.ts @@ -0,0 +1 @@ +export * from './addresses' diff --git a/packages/devtools-sui/src/connection/factory.ts b/packages/devtools-sui/src/connection/factory.ts new file mode 100644 index 0000000000..070aaa0bea --- /dev/null +++ b/packages/devtools-sui/src/connection/factory.ts @@ -0,0 +1,17 @@ +import pMemoize from 'p-memoize' +import { type RpcUrlFactory } from '@layerzerolabs/devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import { SuiClient } from '@mysten/sui/client' +import type { ConnectionFactory } from './types' + +export const defaultRpcUrlFactory: RpcUrlFactory = (eid) => { + throw new Error(`No default Sui RPC URL configured for eid ${eid}. Provide an override via createRpcUrlFactory().`) +} + +export const createRpcUrlFactory = + (overrides: Partial> = {}): RpcUrlFactory => + (eid) => + overrides[eid] ?? process.env.RPC_URL_SUI ?? process.env.RPC_URL_SUI_TESTNET ?? defaultRpcUrlFactory(eid) + +export const createConnectionFactory = (urlFactory: RpcUrlFactory = defaultRpcUrlFactory): ConnectionFactory => + pMemoize(async (eid) => new SuiClient({ url: await urlFactory(eid) })) diff --git a/packages/devtools-sui/src/connection/index.ts b/packages/devtools-sui/src/connection/index.ts new file mode 100644 index 0000000000..97a7b59914 --- /dev/null +++ b/packages/devtools-sui/src/connection/index.ts @@ -0,0 +1,2 @@ +export * from './factory' +export * from './types' diff --git a/packages/devtools-sui/src/connection/types.ts b/packages/devtools-sui/src/connection/types.ts new file mode 100644 index 0000000000..fabebbfe3b --- /dev/null +++ b/packages/devtools-sui/src/connection/types.ts @@ -0,0 +1,4 @@ +import type { SuiClient } from '@mysten/sui/client' +import type { EndpointId } from '@layerzerolabs/lz-definitions' + +export type ConnectionFactory = (eid: EndpointId) => Promise diff --git a/packages/devtools-sui/src/index.ts b/packages/devtools-sui/src/index.ts new file mode 100644 index 0000000000..ded5780815 --- /dev/null +++ b/packages/devtools-sui/src/index.ts @@ -0,0 +1,4 @@ +export * from './common' +export * from './connection' +export * from './omnigraph' +export * from './transactions' diff --git a/packages/devtools-sui/src/omnigraph/coordinates.ts b/packages/devtools-sui/src/omnigraph/coordinates.ts new file mode 100644 index 0000000000..cb72ea241c --- /dev/null +++ b/packages/devtools-sui/src/omnigraph/coordinates.ts @@ -0,0 +1,10 @@ +import type { OmniPoint } from '@layerzerolabs/devtools' +import { ChainType, endpointIdToChainType, type EndpointId } from '@layerzerolabs/lz-definitions' +import { assertSuiAddress } from '../common' + +export const createSuiPoint = (eid: EndpointId, address: string): OmniPoint => { + assertSuiAddress(address) + return { eid, address } +} + +export const isOmniPointOnSui = ({ eid }: OmniPoint): boolean => endpointIdToChainType(eid) === ChainType.SUI diff --git a/packages/devtools-sui/src/omnigraph/index.ts b/packages/devtools-sui/src/omnigraph/index.ts new file mode 100644 index 0000000000..02b48ea20f --- /dev/null +++ b/packages/devtools-sui/src/omnigraph/index.ts @@ -0,0 +1,3 @@ +export * from './coordinates' +export * from './sdk' +export * from './types' diff --git a/packages/devtools-sui/src/omnigraph/sdk.ts b/packages/devtools-sui/src/omnigraph/sdk.ts new file mode 100644 index 0000000000..0ef6e9b82f --- /dev/null +++ b/packages/devtools-sui/src/omnigraph/sdk.ts @@ -0,0 +1,29 @@ +import { SuiClient } from '@mysten/sui/client' +import { Transaction } from '@mysten/sui/transactions' +import { formatOmniPoint, OmniPoint, OmniTransaction } from '@layerzerolabs/devtools' +import { createModuleLogger, type Logger } from '@layerzerolabs/io-devtools' +import type { IOmniSDK } from './types' + +export class OmniSDK implements IOmniSDK { + constructor( + public readonly client: SuiClient, + public readonly point: OmniPoint, + protected readonly logger: Logger = createModuleLogger(`Sui SDK @ ${formatOmniPoint(point)}`) + ) {} + + get label(): string { + return `Sui package @ ${formatOmniPoint(this.point)}` + } + + protected async createTransaction(transaction: Transaction): Promise { + // Serialize the transaction using its JSON representation + // This preserves all transaction data without requiring a sender at build time + // The sender will be set during signing + const serialized = transaction.serialize() + + return { + point: this.point, + data: serialized, + } + } +} diff --git a/packages/devtools-sui/src/omnigraph/types.ts b/packages/devtools-sui/src/omnigraph/types.ts new file mode 100644 index 0000000000..4f75ffaa06 --- /dev/null +++ b/packages/devtools-sui/src/omnigraph/types.ts @@ -0,0 +1,3 @@ +import type { IOmniSDK as IOmniSDKAbstract } from '@layerzerolabs/devtools' + +export interface IOmniSDK extends IOmniSDKAbstract {} diff --git a/packages/devtools-sui/src/transactions/index.ts b/packages/devtools-sui/src/transactions/index.ts new file mode 100644 index 0000000000..18aee9d5ff --- /dev/null +++ b/packages/devtools-sui/src/transactions/index.ts @@ -0,0 +1,2 @@ +export * from './serde' +export * from './signer' diff --git a/packages/devtools-sui/src/transactions/serde.ts b/packages/devtools-sui/src/transactions/serde.ts new file mode 100644 index 0000000000..d42b0f6658 --- /dev/null +++ b/packages/devtools-sui/src/transactions/serde.ts @@ -0,0 +1,5 @@ +import { fromBase64, toBase64 } from '@mysten/bcs' + +export const serializeTransactionBytes = (bytes: Uint8Array): string => toBase64(bytes) + +export const deserializeTransactionBytes = (data: string): Uint8Array => fromBase64(data) diff --git a/packages/devtools-sui/src/transactions/signer.ts b/packages/devtools-sui/src/transactions/signer.ts new file mode 100644 index 0000000000..ec084360ad --- /dev/null +++ b/packages/devtools-sui/src/transactions/signer.ts @@ -0,0 +1,70 @@ +import { Transaction } from '@mysten/sui/transactions' +import { type Signer } from '@mysten/sui/cryptography' +import type { SuiClient } from '@mysten/sui/client' +import { + OmniSigner, + OmniSignerBase, + type OmniSignerFactory, + type OmniTransaction, + type OmniTransactionReceipt, + type OmniTransactionResponse, + OmniPoint, +} from '@layerzerolabs/devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import type { ConnectionFactory } from '../connection' +import { createModuleLogger, type Logger } from '@layerzerolabs/io-devtools' + +export class OmniSignerSui extends OmniSignerBase implements OmniSigner { + constructor( + eid: EndpointId, + public readonly client: SuiClient, + public readonly signer: Signer, + protected readonly logger: Logger = createModuleLogger('OmniSignerSui') + ) { + super(eid) + } + + getPoint(): OmniPoint { + return { eid: this.eid, address: this.signer.toSuiAddress() } + } + + async sign(transaction: OmniTransaction): Promise { + this.assertTransaction(transaction) + + const suiTransaction = Transaction.from(transaction.data) + suiTransaction.setSender(this.signer.toSuiAddress()) + const bytes = await suiTransaction.build({ client: this.client }) + const signature = await this.signer.signTransaction(bytes) + + return signature.signature + } + + async signAndSend(transaction: OmniTransaction): Promise> { + this.assertTransaction(transaction) + + const suiTransaction = Transaction.from(transaction.data) + suiTransaction.setSender(this.signer.toSuiAddress()) + const response = await this.signer.signAndExecuteTransaction({ + transaction: suiTransaction, + client: this.client, + }) + + const digest = response.digest + + return { + transactionHash: digest, + wait: async () => { + await this.client.waitForTransaction({ digest }) + return { transactionHash: digest } + }, + } + } +} + +export const createSuiSignerFactory = + ( + signer: Signer, + connectionFactory: ConnectionFactory + ): OmniSignerFactory>> => + async (eid: EndpointId) => + new OmniSignerSui(eid, await connectionFactory(eid), signer) diff --git a/packages/devtools-sui/tsconfig.json b/packages/devtools-sui/tsconfig.json new file mode 100644 index 0000000000..75eba9f992 --- /dev/null +++ b/packages/devtools-sui/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/devtools-sui/tsup.config.ts b/packages/devtools-sui/tsup.config.ts new file mode 100644 index 0000000000..cbe50c7c92 --- /dev/null +++ b/packages/devtools-sui/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + sourcemap: true, + clean: true, +}) diff --git a/packages/devtools/src/common/bytes.ts b/packages/devtools/src/common/bytes.ts index d890dd6c36..df9f5004b6 100644 --- a/packages/devtools/src/common/bytes.ts +++ b/packages/devtools/src/common/bytes.ts @@ -100,6 +100,7 @@ export const normalizePeer = (address: OmniAddress | null | undefined, eid: Endp return bs58.decode(address) case ChainType.APTOS: + case ChainType.SUI: case ChainType.EVM: case ChainType.STARKNET: return toBytes32(fromHex(address)) @@ -131,6 +132,7 @@ export const denormalizePeer = (bytes: Uint8Array | null | undefined, eid: Endpo return bs58.encode(toBytes32(bytes)) case ChainType.APTOS: + case ChainType.SUI: case ChainType.STARKNET: return toHex(toBytes32(bytes)) From 2c3ef00209e48c564ac3309769419b6728dc7cf1 Mon Sep 17 00:00:00 2001 From: Krak Date: Mon, 26 Jan 2026 09:37:52 -0800 Subject: [PATCH 2/8] feat(protocol-devtools-sui): add EndpointV2 SDK for Sui - Add EndpointV2 SDK implementing IEndpointV2 interface - Add Uln302 SDK for ULN configuration management - Fix setConfig to call populateSetConfigTransaction (Move result consumption) - Add graceful handling of missing configurations Key fixes for lz:oapp:wire: - setConfigMoveCall returns Call that must be consumed - populateSetConfigTransaction() adds necessary follow-up move call - Missing config queries return defaults instead of throwing --- packages/protocol-devtools-sui/.eslintignore | 2 + packages/protocol-devtools-sui/.eslintrc.json | 3 + .../protocol-devtools-sui/.prettierignore | 2 + packages/protocol-devtools-sui/CHANGELOG.md | 5 + packages/protocol-devtools-sui/README.md | 19 + packages/protocol-devtools-sui/bin/test | 7 + packages/protocol-devtools-sui/jest.config.js | 14 + packages/protocol-devtools-sui/jest.setup.js | 4 + packages/protocol-devtools-sui/package.json | 79 ++++ .../protocol-devtools-sui/src/addresses.ts | 16 + .../src/endpointv2/index.ts | 1 + .../src/endpointv2/sdk.ts | 383 ++++++++++++++++++ packages/protocol-devtools-sui/src/index.ts | 3 + .../protocol-devtools-sui/src/uln302/index.ts | 1 + .../protocol-devtools-sui/src/uln302/sdk.ts | 193 +++++++++ packages/protocol-devtools-sui/tsconfig.json | 13 + packages/protocol-devtools-sui/tsup.config.ts | 14 + 17 files changed, 759 insertions(+) create mode 100644 packages/protocol-devtools-sui/.eslintignore create mode 100644 packages/protocol-devtools-sui/.eslintrc.json create mode 100644 packages/protocol-devtools-sui/.prettierignore create mode 100644 packages/protocol-devtools-sui/CHANGELOG.md create mode 100644 packages/protocol-devtools-sui/README.md create mode 100755 packages/protocol-devtools-sui/bin/test create mode 100644 packages/protocol-devtools-sui/jest.config.js create mode 100644 packages/protocol-devtools-sui/jest.setup.js create mode 100644 packages/protocol-devtools-sui/package.json create mode 100644 packages/protocol-devtools-sui/src/addresses.ts create mode 100644 packages/protocol-devtools-sui/src/endpointv2/index.ts create mode 100644 packages/protocol-devtools-sui/src/endpointv2/sdk.ts create mode 100644 packages/protocol-devtools-sui/src/index.ts create mode 100644 packages/protocol-devtools-sui/src/uln302/index.ts create mode 100644 packages/protocol-devtools-sui/src/uln302/sdk.ts create mode 100644 packages/protocol-devtools-sui/tsconfig.json create mode 100644 packages/protocol-devtools-sui/tsup.config.ts diff --git a/packages/protocol-devtools-sui/.eslintignore b/packages/protocol-devtools-sui/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/packages/protocol-devtools-sui/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/protocol-devtools-sui/.eslintrc.json b/packages/protocol-devtools-sui/.eslintrc.json new file mode 100644 index 0000000000..be97c53fbb --- /dev/null +++ b/packages/protocol-devtools-sui/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/protocol-devtools-sui/.prettierignore b/packages/protocol-devtools-sui/.prettierignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/packages/protocol-devtools-sui/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/packages/protocol-devtools-sui/CHANGELOG.md b/packages/protocol-devtools-sui/CHANGELOG.md new file mode 100644 index 0000000000..dbbf0d5cfa --- /dev/null +++ b/packages/protocol-devtools-sui/CHANGELOG.md @@ -0,0 +1,5 @@ +# @layerzerolabs/protocol-devtools-sui + +## 0.1.0 + +- Initial release. diff --git a/packages/protocol-devtools-sui/README.md b/packages/protocol-devtools-sui/README.md new file mode 100644 index 0000000000..f533d06957 --- /dev/null +++ b/packages/protocol-devtools-sui/README.md @@ -0,0 +1,19 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/protocol-devtools-sui

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +Utilities for working with LayerZero Sui protocol packages. diff --git a/packages/protocol-devtools-sui/bin/test b/packages/protocol-devtools-sui/bin/test new file mode 100755 index 0000000000..2f9caec4e7 --- /dev/null +++ b/packages/protocol-devtools-sui/bin/test @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ -z "${LZ_DEVTOOLS_ENABLE_SUI_TESTS}" ]; then + echo 'Sui tests can be enabled by setting LZ_DEVTOOLS_ENABLE_SUI_TESTS environment variable to a non-empty value' +else + jest --ci "$@" +fi diff --git a/packages/protocol-devtools-sui/jest.config.js b/packages/protocol-devtools-sui/jest.config.js new file mode 100644 index 0000000000..04c8477342 --- /dev/null +++ b/packages/protocol-devtools-sui/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + cache: false, + reporters: [['github-actions', { silent: false }], 'default'], + testEnvironment: 'node', + testTimeout: 60_000, + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, +}; diff --git a/packages/protocol-devtools-sui/jest.setup.js b/packages/protocol-devtools-sui/jest.setup.js new file mode 100644 index 0000000000..956faa4d5c --- /dev/null +++ b/packages/protocol-devtools-sui/jest.setup.js @@ -0,0 +1,4 @@ +import * as jestExtended from 'jest-extended'; + +// add all jest-extended matchers +expect.extend(jestExtended); diff --git a/packages/protocol-devtools-sui/package.json b/packages/protocol-devtools-sui/package.json new file mode 100644 index 0000000000..abc9f28fc3 --- /dev/null +++ b/packages/protocol-devtools-sui/package.json @@ -0,0 +1,79 @@ +{ + "name": "@layerzerolabs/protocol-devtools-sui", + "version": "0.1.0", + "description": "Utilities for LayerZero Sui protocol packages", + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/devtools.git", + "directory": "packages/protocol-devtools-sui" + }, + "license": "MIT", + "sideEffects": false, + "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": "$npm_execpath tsc --noEmit", + "build": "$npm_execpath tsup", + "clean": "rm -rf dist", + "dev": "$npm_execpath tsup --watch", + "lint": "$npm_execpath eslint '**/*.{js,ts,json}'", + "lint:fix": "eslint --fix '**/*.{js,ts,json}'", + "test": "./bin/test" + }, + "dependencies": { + "p-memoize": "~4.0.4" + }, + "devDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-sui": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-sui-sdk-v2": "^3.0.156", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@layerzerolabs/test-devtools": "~0.4.7", + "@layerzerolabs/test-devtools-sui": "~0.0.1", + "@mysten/sui": "^1.45.2", + "@swc/core": "^1.4.0", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.12", + "fast-check": "^3.15.1", + "jest": "^29.7.0", + "jest-extended": "^4.0.2", + "ts-node": "^10.9.2", + "tslib": "~2.6.2", + "tsup": "~8.0.1", + "typescript": "^5.4.4", + "zod": "^3.22.4" + }, + "peerDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-sui": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-sui-sdk-v2": "^3.0.156", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@mysten/sui": "^1.45.2", + "zod": "^3.22.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/protocol-devtools-sui/src/addresses.ts b/packages/protocol-devtools-sui/src/addresses.ts new file mode 100644 index 0000000000..0b4322ebf0 --- /dev/null +++ b/packages/protocol-devtools-sui/src/addresses.ts @@ -0,0 +1,16 @@ +import { EndpointId } from '@layerzerolabs/lz-definitions' + +export const SUI_ENDPOINT_V2_ADDRESSES: Partial> = { + [EndpointId.SUI_V2_MAINNET]: '0x31beaef889b08b9c3b37d19280fc1f8b75bae5b2de2410fc3120f403e9a36dac', + [EndpointId.SUI_V2_TESTNET]: '0xabf9629418d997fcc742a5ca22820241b72fb53691f010bc964eb49b4bd2263a', +} + +export const SUI_ULN_302_ADDRESSES: Partial> = { + [EndpointId.SUI_V2_MAINNET]: '0x3ce7457bed48ad23ee5d611dd3172ae4fbd0a22ea0e846782a7af224d905dbb0', + [EndpointId.SUI_V2_TESTNET]: '0xf5d69c7b0922ce0ab4540525fbc66ca25ce9f092c64b032b91e4c5625ea0fb24', +} + +export const SUI_EXECUTOR_ADDRESSES: Partial> = { + [EndpointId.SUI_V2_MAINNET]: '0xde7fe1a6648d587fcc991f124f3aa5b6389340610804108094d5c5fbf61d1989', + [EndpointId.SUI_V2_TESTNET]: '0xb9fdc6748fb939095e249b22717d564edf890681e387131d6c525d867d30f834', +} diff --git a/packages/protocol-devtools-sui/src/endpointv2/index.ts b/packages/protocol-devtools-sui/src/endpointv2/index.ts new file mode 100644 index 0000000000..7db67b18a1 --- /dev/null +++ b/packages/protocol-devtools-sui/src/endpointv2/index.ts @@ -0,0 +1 @@ +export * from './sdk' diff --git a/packages/protocol-devtools-sui/src/endpointv2/sdk.ts b/packages/protocol-devtools-sui/src/endpointv2/sdk.ts new file mode 100644 index 0000000000..7212abbdb3 --- /dev/null +++ b/packages/protocol-devtools-sui/src/endpointv2/sdk.ts @@ -0,0 +1,383 @@ +import { Transaction } from '@mysten/sui/transactions' +import type { + IEndpointV2, + IUlnRead, + MessageParams, + MessagingFee, + SetConfigParam, + Timeout, + Uln302ConfigType, + Uln302ExecutorConfig, + Uln302SetExecutorConfig, + Uln302SetUlnConfig, + Uln302UlnConfig, + Uln302UlnUserConfig, + UlnReadSetUlnConfig, + UlnReadUlnConfig, + UlnReadUlnUserConfig, +} from '@layerzerolabs/protocol-devtools' +import { endpointIdToStage, Stage, type EndpointId } from '@layerzerolabs/lz-definitions' +import { type Bytes32, type OmniAddress, type OmniTransaction, type PossiblyBytes } from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-sui' +import { CONFIG_TYPE, ExecutorConfigBcs, OAppUlnConfigBcs, SDK } from '@layerzerolabs/lz-sui-sdk-v2' +import type { Endpoint, OApp, ExecutorConfig, OAppUlnConfig } from '@layerzerolabs/lz-sui-sdk-v2' +import type { Uln302 } from '../uln302' + +export class EndpointV2 extends OmniSDK implements IEndpointV2 { + private sdk?: SDK + private oapp?: OApp + private endpoint?: Endpoint + + async getUln302SDK(address: OmniAddress): Promise { + this.logger.debug(`Getting Uln302 SDK for address ${address}`) + const { Uln302 } = await import('../uln302') + return new Uln302(this.client, { eid: this.point.eid, address }) + } + + async getUlnReadSDK(_address: OmniAddress): Promise { + throw new Error('ULN Read functionality is not supported for Sui.') + } + + async getDelegate(_oapp: OmniAddress): Promise { + return this.getEndpoint().getDelegate(_oapp) + } + + async isDelegate(_oapp: OmniAddress, _delegate: OmniAddress): Promise { + const delegate = await this.getDelegate(_oapp) + return delegate === _delegate + } + + async getDefaultReceiveLibrary(_eid: EndpointId): Promise { + return this.getEndpoint().getDefaultReceiveLibrary(_eid) + } + + async setDefaultReceiveLibrary( + _eid: EndpointId, + _uln: OmniAddress | null | undefined, + _gracePeriod: bigint = 0n + ): Promise { + const tx = new Transaction() + this.getEndpoint().setDefaultReceiveLibraryMoveCall(tx, _eid, _uln ?? '0x0', _gracePeriod) + return this.createTransaction(tx) + } + + async getDefaultSendLibrary(_eid: EndpointId): Promise { + return this.getEndpoint().getDefaultSendLibrary(_eid) + } + + async setDefaultSendLibrary(_eid: EndpointId, _uln: OmniAddress | null | undefined): Promise { + const tx = new Transaction() + this.getEndpoint().setDefaultSendLibraryMoveCall(tx, _eid, _uln ?? '0x0') + return this.createTransaction(tx) + } + + async isRegisteredLibrary(_uln: OmniAddress): Promise { + return this.notImplemented('isRegisteredLibrary') + } + + async registerLibrary(_uln: OmniAddress): Promise { + return this.notImplemented('registerLibrary') + } + + async isBlockedLibrary(_uln: OmniAddress): Promise { + return this.notImplemented('isBlockedLibrary') + } + + async getSendLibrary(_sender: OmniAddress, _dstEid: EndpointId): Promise { + const [library] = await this.getEndpoint().getSendLibrary(_sender, _dstEid) + return library + } + + async getReceiveLibrary( + _receiver: OmniAddress, + _srcEid: EndpointId + ): Promise<[address: Bytes32 | undefined, isDefault: boolean]> { + const [library, isDefault] = await this.getEndpoint().getReceiveLibrary(_receiver, _srcEid) + return [library, isDefault] + } + + async getDefaultReceiveLibraryTimeout(_eid: EndpointId): Promise { + const timeout = await this.getEndpoint().getDefaultReceiveLibraryTimeout(_eid) + if (!timeout) { + return { lib: '0x0', expiry: 0n } + } + return { lib: timeout.fallbackLib, expiry: timeout.expiry } + } + + async getReceiveLibraryTimeout(_receiver: OmniAddress, _srcEid: EndpointId): Promise { + const timeout = await this.getEndpoint().getReceiveLibraryTimeout(_receiver, _srcEid) + if (!timeout) { + return { lib: '0x0', expiry: 0n } + } + return { lib: timeout.fallbackLib, expiry: timeout.expiry } + } + + async setSendLibrary(_oapp: OmniAddress, _eid: EndpointId, _uln: OmniAddress): Promise { + const tx = new Transaction() + await this.getOApp(_oapp).setSendLibraryMoveCall(tx, _eid, _uln) + return this.createTransaction(tx) + } + + async isDefaultSendLibrary(_sender: PossiblyBytes, _dstEid: EndpointId): Promise { + const [_, isDefault] = await this.getEndpoint().getSendLibrary(String(_sender), _dstEid) + return isDefault + } + + async setReceiveLibrary( + _oapp: OmniAddress, + _eid: EndpointId, + _uln: OmniAddress, + _gracePeriod: bigint + ): Promise { + const tx = new Transaction() + await this.getOApp(_oapp).setReceiveLibraryMoveCall(tx, _eid, _uln, _gracePeriod) + return this.createTransaction(tx) + } + + async setReceiveLibraryTimeout( + _oapp: OmniAddress, + _eid: EndpointId, + _uln: OmniAddress, + _expiry: bigint + ): Promise { + const tx = new Transaction() + await this.getOApp(_oapp).setReceiveLibraryTimeoutMoveCall(tx, _eid, _uln, _expiry) + return this.createTransaction(tx) + } + + async getExecutorConfig(_oapp: PossiblyBytes, _uln: OmniAddress, _eid: EndpointId): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getExecutorConfig(_eid, String(_oapp)) + } + + async getAppExecutorConfig( + _oapp: PossiblyBytes, + _uln: OmniAddress, + _eid: EndpointId + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getAppExecutorConfig(_eid, String(_oapp)) + } + + async hasAppExecutorConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _config: Uln302ExecutorConfig + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.hasAppExecutorConfig(_eid, _oapp, _config) + } + + async setExecutorConfig( + _oapp: PossiblyBytes, + _uln: PossiblyBytes, + _setExecutorConfig: Uln302SetExecutorConfig[] + ): Promise { + return this.createConfigTransactions(String(_oapp), String(_uln), _setExecutorConfig, CONFIG_TYPE.EXECUTOR) + } + + async getUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _type: Uln302ConfigType + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getUlnConfig(_eid, _oapp, _type) + } + + async getAppUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _type: Uln302ConfigType + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getAppUlnConfig(_eid, _oapp, _type) + } + + async getAppUlnReadConfig(_oapp: OmniAddress, _uln: OmniAddress, _channelId: number): Promise { + throw new Error('ULN Read functionality is not supported for Sui.') + } + + async hasAppUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _config: Uln302UlnUserConfig, + _type: Uln302ConfigType + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.hasAppUlnConfig(_eid, _oapp, _config, _type) + } + + async hasAppUlnReadConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _channelId: number, + _config: UlnReadUlnUserConfig + ): Promise { + throw new Error('ULN Read functionality is not supported for Sui.') + } + + async setUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _setUlnConfig: Uln302SetUlnConfig[] + ): Promise { + return this.createConfigTransactions(_oapp, _uln, _setUlnConfig, CONFIG_TYPE.SEND_ULN) + } + + async setUlnReadConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _setUlnConfig: UlnReadSetUlnConfig[] + ): Promise { + throw new Error('ULN Read functionality is not supported for Sui.') + } + + async getUlnConfigParams(_uln: OmniAddress, _setUlnConfig: Uln302SetUlnConfig[]): Promise { + return _setUlnConfig.map(({ eid, ulnConfig, type }) => ({ + eid, + configType: type === 'send' ? CONFIG_TYPE.SEND_ULN : CONFIG_TYPE.RECEIVE_ULN, + config: this.serializeUlnConfig(ulnConfig), + })) + } + + async getUlnReadConfigParams(_uln: OmniAddress, _setUlnConfig: UlnReadSetUlnConfig[]): Promise { + throw new Error('ULN Read functionality is not supported for Sui.') + } + + async getExecutorConfigParams( + _uln: OmniAddress, + _setExecutorConfig: Uln302SetExecutorConfig[] + ): Promise { + return _setExecutorConfig.map(({ eid, executorConfig }) => ({ + eid, + configType: CONFIG_TYPE.EXECUTOR, + config: this.serializeExecutorConfig(executorConfig), + })) + } + + async setConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _setConfigParam: SetConfigParam[] + ): Promise { + const txs: OmniTransaction[] = [] + for (const param of _setConfigParam) { + const tx = new Transaction() + const setConfigCall = await this.getOApp(_oapp).setConfigMoveCall( + tx, + _uln, + param.eid, + param.configType, + param.config as Uint8Array + ) + // The setConfigMoveCall returns a Call that must be + // populated using the endpoint's populateSetConfigTransaction to build the complete transaction + await this.getEndpoint().populateSetConfigTransaction(tx, setConfigCall) + txs.push(await this.createTransaction(tx)) + } + return txs + } + + async quote(_params: MessageParams, _sender: OmniAddress): Promise { + return this.notImplemented('quote') + } + + private notImplemented(method: string): never { + throw new TypeError(`${method}() not implemented on Sui Endpoint SDK`) + } + + private getSdk(): SDK { + if (!this.sdk) { + const stage = endpointIdToStage(this.point.eid) as Stage + this.sdk = new SDK({ client: this.client, stage }) + } + return this.sdk + } + + private getOApp(callCapId: OmniAddress = this.point.address): OApp { + if (callCapId === this.point.address) { + if (!this.oapp) { + this.oapp = this.getSdk().getOApp(callCapId) + } + return this.oapp + } + return this.getSdk().getOApp(callCapId) + } + + private getEndpoint(): Endpoint { + if (!this.endpoint) { + this.endpoint = this.getSdk().getEndpoint() + } + return this.endpoint + } + + private serializeExecutorConfig(config: Uln302ExecutorConfig): Uint8Array { + const executorConfig: ExecutorConfig = { + maxMessageSize: config.maxMessageSize, + executor: config.executor, + } + return ExecutorConfigBcs.serialize({ + max_message_size: executorConfig.maxMessageSize, + executor: executorConfig.executor, + }).toBytes() + } + + private serializeUlnConfig(config: Uln302UlnUserConfig): Uint8Array { + const useDefaultConfirmations = config.confirmations == null + const useDefaultRequiredDvns = config.requiredDVNs.length === 0 + const useDefaultOptionalDvns = config.optionalDVNs == null + const ulnConfig: OAppUlnConfig = { + useDefaultConfirmations, + useDefaultRequiredDvns, + useDefaultOptionalDvns, + ulnConfig: { + confirmations: config.confirmations ?? 0n, + requiredDvns: config.requiredDVNs, + optionalDvns: config.optionalDVNs ?? [], + optionalDvnThreshold: config.optionalDVNThreshold ?? 0, + }, + } + return OAppUlnConfigBcs.serialize({ + use_default_confirmations: ulnConfig.useDefaultConfirmations, + use_default_required_dvns: ulnConfig.useDefaultRequiredDvns, + use_default_optional_dvns: ulnConfig.useDefaultOptionalDvns, + uln_config: { + confirmations: ulnConfig.ulnConfig.confirmations, + required_dvns: ulnConfig.ulnConfig.requiredDvns, + optional_dvns: ulnConfig.ulnConfig.optionalDvns, + optional_dvn_threshold: ulnConfig.ulnConfig.optionalDvnThreshold, + }, + }).toBytes() + } + + private async createConfigTransactions( + oapp: OmniAddress, + uln: OmniAddress, + configs: Uln302SetExecutorConfig[] | Uln302SetUlnConfig[], + configType: number + ): Promise { + const txs: OmniTransaction[] = [] + for (const config of configs) { + const tx = new Transaction() + let setConfigCall + if ('executorConfig' in config) { + const bytes = this.serializeExecutorConfig(config.executorConfig) + setConfigCall = await this.getOApp(oapp).setConfigMoveCall(tx, uln, config.eid, configType, bytes) + } else { + const bytes = this.serializeUlnConfig(config.ulnConfig) + const type = config.type === 'send' ? CONFIG_TYPE.SEND_ULN : CONFIG_TYPE.RECEIVE_ULN + setConfigCall = await this.getOApp(oapp).setConfigMoveCall(tx, uln, config.eid, type, bytes) + } + // The setConfigMoveCall returns a Call that must be + // populated using the endpoint's populateSetConfigTransaction to build the complete transaction + await this.getEndpoint().populateSetConfigTransaction(tx, setConfigCall) + txs.push(await this.createTransaction(tx)) + } + return txs + } +} diff --git a/packages/protocol-devtools-sui/src/index.ts b/packages/protocol-devtools-sui/src/index.ts new file mode 100644 index 0000000000..5b9e0f3793 --- /dev/null +++ b/packages/protocol-devtools-sui/src/index.ts @@ -0,0 +1,3 @@ +export * from './addresses' +export * from './endpointv2' +export * from './uln302' diff --git a/packages/protocol-devtools-sui/src/uln302/index.ts b/packages/protocol-devtools-sui/src/uln302/index.ts new file mode 100644 index 0000000000..7db67b18a1 --- /dev/null +++ b/packages/protocol-devtools-sui/src/uln302/index.ts @@ -0,0 +1 @@ +export * from './sdk' diff --git a/packages/protocol-devtools-sui/src/uln302/sdk.ts b/packages/protocol-devtools-sui/src/uln302/sdk.ts new file mode 100644 index 0000000000..9c99836fa4 --- /dev/null +++ b/packages/protocol-devtools-sui/src/uln302/sdk.ts @@ -0,0 +1,193 @@ +import { Transaction } from '@mysten/sui/transactions' +import type { + IUln302, + Uln302ConfigType, + Uln302ExecutorConfig, + Uln302UlnConfig, + Uln302UlnUserConfig, +} from '@layerzerolabs/protocol-devtools' +import { endpointIdToStage, Stage, type EndpointId } from '@layerzerolabs/lz-definitions' +import type { OmniAddress, OmniTransaction } from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-sui' +import { SDK } from '@layerzerolabs/lz-sui-sdk-v2' +import type { ExecutorConfig, OAppUlnConfig, UlnConfig, Uln302 as SuiUln302 } from '@layerzerolabs/lz-sui-sdk-v2' + +export class Uln302 extends OmniSDK implements IUln302 { + private sdk?: SDK + private uln?: SuiUln302 + + async getUlnConfig( + _eid: EndpointId, + _address: OmniAddress | null | undefined, + _type: Uln302ConfigType + ): Promise { + if (_type === 'send') { + const config = _address + ? await this.getUln().getEffectiveSendUlnConfig(_address, _eid) + : await this.getUln().getDefaultSendUlnConfig(_eid) + return this.toUlnConfig(config) + } + + const config = _address + ? await this.getUln().getEffectiveReceiveUlnConfig(_address, _eid) + : await this.getUln().getDefaultReceiveUlnConfig(_eid) + return this.toUlnConfig(config) + } + + async getAppUlnConfig(_eid: EndpointId, _address: OmniAddress, _type: Uln302ConfigType): Promise { + try { + const config: OAppUlnConfig = + _type === 'send' + ? await this.getUln().getOAppSendUlnConfig(_address, _eid) + : await this.getUln().getOAppReceiveUlnConfig(_address, _eid) + return this.toUlnConfig(config.ulnConfig) + } catch (error) { + // If the config doesn't exist, return empty config + if (this.isMissingSuiConfig(error)) { + return this.toUlnConfig({ + confirmations: 0n, + requiredDvns: [], + optionalDvns: [], + optionalDvnThreshold: 0, + }) + } + throw error + } + } + + async hasAppUlnConfig( + _eid: EndpointId, + _oapp: OmniAddress, + _config: Uln302UlnUserConfig, + _type: Uln302ConfigType + ): Promise { + const current = await this.getAppUlnConfig(_eid, _oapp, _type) + const required = { + confirmations: _config.confirmations ?? current.confirmations, + requiredDVNs: _config.requiredDVNs, + optionalDVNs: _config.optionalDVNs ?? [], + optionalDVNThreshold: _config.optionalDVNThreshold ?? 0, + } + return ( + current.confirmations === required.confirmations && + this.equalStringArrays(current.requiredDVNs, required.requiredDVNs) && + this.equalStringArrays(current.optionalDVNs, required.optionalDVNs) && + current.optionalDVNThreshold === required.optionalDVNThreshold + ) + } + + async setDefaultUlnConfig(_eid: EndpointId, _config: Uln302UlnUserConfig): Promise { + const tx = new Transaction() + const ulnConfig: UlnConfig = { + confirmations: _config.confirmations ?? 0n, + requiredDvns: _config.requiredDVNs, + optionalDvns: _config.optionalDVNs ?? [], + optionalDvnThreshold: _config.optionalDVNThreshold ?? 0, + } + this.getUln().setDefaultSendUlnConfigMoveCall(tx, _eid, ulnConfig) + this.getUln().setDefaultReceiveUlnConfigMoveCall(tx, _eid, ulnConfig) + return this.createTransaction(tx) + } + + async getExecutorConfig( + _eid: EndpointId, + _address?: OmniAddress | null | undefined + ): Promise { + const config = _address + ? await this.getUln().getEffectiveExecutorConfig(_address, _eid) + : await this.getUln().getDefaultExecutorConfig(_eid) + return { + maxMessageSize: Number(config.maxMessageSize), + executor: config.executor, + } + } + + async getAppExecutorConfig(_eid: EndpointId, _address: OmniAddress): Promise { + try { + const config = await this.getUln().getOAppExecutorConfig(_address, _eid) + return { + maxMessageSize: Number(config.maxMessageSize), + executor: config.executor, + } + } catch (error) { + // If the config doesn't exist, return empty config + if (this.isMissingSuiConfig(error)) { + return { + maxMessageSize: 0, + executor: '', + } + } + throw error + } + } + + async hasAppExecutorConfig(_eid: EndpointId, _oapp: OmniAddress, _config: Uln302ExecutorConfig): Promise { + const current = await this.getAppExecutorConfig(_eid, _oapp) + return current.maxMessageSize === _config.maxMessageSize && current.executor === _config.executor + } + + async setDefaultExecutorConfig(_eid: EndpointId, _config: Uln302ExecutorConfig): Promise { + const tx = new Transaction() + const executorConfig: ExecutorConfig = { + maxMessageSize: _config.maxMessageSize, + executor: _config.executor, + } + this.getUln().setDefaultExecutorConfigMoveCall(tx, _eid, executorConfig) + return this.createTransaction(tx) + } + + private notImplemented(method: string): never { + throw new TypeError(`${method}() not implemented on Sui ULN302 SDK`) + } + + private getSdk(): SDK { + if (!this.sdk) { + const stage = endpointIdToStage(this.point.eid) as Stage + this.sdk = new SDK({ client: this.client, stage }) + } + return this.sdk + } + + private getUln(): SuiUln302 { + if (!this.uln) { + this.uln = this.getSdk().getUln302() + } + return this.uln + } + + private toUlnConfig(config: UlnConfig): Uln302UlnConfig { + return { + confirmations: BigInt(config.confirmations), + requiredDVNs: config.requiredDvns, + requiredDVNCount: config.requiredDvns.length, + optionalDVNs: config.optionalDvns, + optionalDVNThreshold: config.optionalDvnThreshold, + } + } + + private equalStringArrays(left: string[], right: string[]): boolean { + if (left.length !== right.length) { + return false + } + const sortedLeft = [...left].sort() + const sortedRight = [...right].sort() + return sortedLeft.every((value, index) => value === sortedRight[index]) + } + + private isMissingSuiConfig(error: unknown): boolean { + const message = + typeof error === 'string' + ? error.toLowerCase() + : error && typeof error === 'object' && 'message' in error + ? String((error as { message?: unknown }).message).toLowerCase() + : '' + if (!message) { + return false + } + // Move abort errors indicate config doesn't exist + return ( + message.includes('move abort') && + (message.includes('send_uln') || message.includes('receive_uln') || message.includes('executor')) + ) + } +} diff --git a/packages/protocol-devtools-sui/tsconfig.json b/packages/protocol-devtools-sui/tsconfig.json new file mode 100644 index 0000000000..12774c873d --- /dev/null +++ b/packages/protocol-devtools-sui/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["dist", "node_modules"], + "include": ["src", "test", "*.config.ts"], + "compilerOptions": { + "target": "es2020", + "experimentalDecorators": true, + "types": ["node", "jest"], + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/protocol-devtools-sui/tsup.config.ts b/packages/protocol-devtools-sui/tsup.config.ts new file mode 100644 index 0000000000..7ef46a5ad1 --- /dev/null +++ b/packages/protocol-devtools-sui/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + entry: ['src/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, +]) From 94d6ef2373cfdb85dafcd531718b71d2ee5e0873 Mon Sep 17 00:00:00 2001 From: Krak Date: Mon, 26 Jan 2026 09:38:02 -0800 Subject: [PATCH 3/8] feat(ua-devtools-sui): add OFT SDK for Sui - Add OFT SDK implementing IOApp interface - Fix setPeer to pad EVM addresses (20 bytes) to 32 bytes - Fix hasPeer to use areBytes32Equal for normalized comparison - Add isMissingSuiPeer helper for graceful error handling Key fixes for lz:oapp:wire: - EVM addresses must be right-padded with zeros to 32 bytes for Sui - Address comparison must normalize both addresses before comparing - Missing peer/enforced_options errors return defaults instead of throwing --- packages/ua-devtools-sui/.eslintignore | 2 + packages/ua-devtools-sui/.eslintrc.json | 3 + packages/ua-devtools-sui/.prettierignore | 2 + packages/ua-devtools-sui/CHANGELOG.md | 5 + packages/ua-devtools-sui/README.md | 19 ++ packages/ua-devtools-sui/bin/test | 7 + packages/ua-devtools-sui/jest.config.js | 14 ++ packages/ua-devtools-sui/jest.setup.js | 4 + packages/ua-devtools-sui/package.json | 83 +++++++++ packages/ua-devtools-sui/src/index.ts | 1 + packages/ua-devtools-sui/src/oft/config.ts | 44 +++++ packages/ua-devtools-sui/src/oft/factory.ts | 15 ++ packages/ua-devtools-sui/src/oft/index.ts | 3 + packages/ua-devtools-sui/src/oft/sdk.ts | 182 ++++++++++++++++++++ packages/ua-devtools-sui/tsconfig.json | 13 ++ packages/ua-devtools-sui/tsup.config.ts | 14 ++ 16 files changed, 411 insertions(+) create mode 100644 packages/ua-devtools-sui/.eslintignore create mode 100644 packages/ua-devtools-sui/.eslintrc.json create mode 100644 packages/ua-devtools-sui/.prettierignore create mode 100644 packages/ua-devtools-sui/CHANGELOG.md create mode 100644 packages/ua-devtools-sui/README.md create mode 100755 packages/ua-devtools-sui/bin/test create mode 100644 packages/ua-devtools-sui/jest.config.js create mode 100644 packages/ua-devtools-sui/jest.setup.js create mode 100644 packages/ua-devtools-sui/package.json create mode 100644 packages/ua-devtools-sui/src/index.ts create mode 100644 packages/ua-devtools-sui/src/oft/config.ts create mode 100644 packages/ua-devtools-sui/src/oft/factory.ts create mode 100644 packages/ua-devtools-sui/src/oft/index.ts create mode 100644 packages/ua-devtools-sui/src/oft/sdk.ts create mode 100644 packages/ua-devtools-sui/tsconfig.json create mode 100644 packages/ua-devtools-sui/tsup.config.ts diff --git a/packages/ua-devtools-sui/.eslintignore b/packages/ua-devtools-sui/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/packages/ua-devtools-sui/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/ua-devtools-sui/.eslintrc.json b/packages/ua-devtools-sui/.eslintrc.json new file mode 100644 index 0000000000..be97c53fbb --- /dev/null +++ b/packages/ua-devtools-sui/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/ua-devtools-sui/.prettierignore b/packages/ua-devtools-sui/.prettierignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/packages/ua-devtools-sui/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/packages/ua-devtools-sui/CHANGELOG.md b/packages/ua-devtools-sui/CHANGELOG.md new file mode 100644 index 0000000000..905faf0cc2 --- /dev/null +++ b/packages/ua-devtools-sui/CHANGELOG.md @@ -0,0 +1,5 @@ +# @layerzerolabs/ua-devtools-sui + +## 0.1.0 + +- Initial release. diff --git a/packages/ua-devtools-sui/README.md b/packages/ua-devtools-sui/README.md new file mode 100644 index 0000000000..43cf28ef41 --- /dev/null +++ b/packages/ua-devtools-sui/README.md @@ -0,0 +1,19 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/ua-devtools-sui

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +Utilities for working with LayerZero Sui contracts. diff --git a/packages/ua-devtools-sui/bin/test b/packages/ua-devtools-sui/bin/test new file mode 100755 index 0000000000..2f9caec4e7 --- /dev/null +++ b/packages/ua-devtools-sui/bin/test @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ -z "${LZ_DEVTOOLS_ENABLE_SUI_TESTS}" ]; then + echo 'Sui tests can be enabled by setting LZ_DEVTOOLS_ENABLE_SUI_TESTS environment variable to a non-empty value' +else + jest --ci "$@" +fi diff --git a/packages/ua-devtools-sui/jest.config.js b/packages/ua-devtools-sui/jest.config.js new file mode 100644 index 0000000000..04c8477342 --- /dev/null +++ b/packages/ua-devtools-sui/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + cache: false, + reporters: [['github-actions', { silent: false }], 'default'], + testEnvironment: 'node', + testTimeout: 60_000, + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, +}; diff --git a/packages/ua-devtools-sui/jest.setup.js b/packages/ua-devtools-sui/jest.setup.js new file mode 100644 index 0000000000..956faa4d5c --- /dev/null +++ b/packages/ua-devtools-sui/jest.setup.js @@ -0,0 +1,4 @@ +import * as jestExtended from 'jest-extended'; + +// add all jest-extended matchers +expect.extend(jestExtended); diff --git a/packages/ua-devtools-sui/package.json b/packages/ua-devtools-sui/package.json new file mode 100644 index 0000000000..cdad9397d7 --- /dev/null +++ b/packages/ua-devtools-sui/package.json @@ -0,0 +1,83 @@ +{ + "name": "@layerzerolabs/ua-devtools-sui", + "version": "0.1.0", + "description": "Utilities for LayerZero Sui projects", + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/devtools.git", + "directory": "packages/ua-devtools-sui" + }, + "license": "MIT", + "sideEffects": false, + "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": "$npm_execpath tsc --noEmit", + "build": "$npm_execpath tsup", + "clean": "rm -rf dist", + "dev": "$npm_execpath tsup --watch", + "lint": "$npm_execpath eslint '**/*.{js,ts,json}'", + "lint:fix": "eslint --fix '**/*.{js,ts,json}'", + "test": "./bin/test" + }, + "dependencies": { + "p-memoize": "~4.0.4" + }, + "devDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-sui": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-sui-oft-sdk-v2": "^3.0.156", + "@layerzerolabs/lz-sui-sdk-v2": "^3.0.156", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@layerzerolabs/protocol-devtools-sui": "~0.1.0", + "@layerzerolabs/test-devtools": "~0.4.7", + "@layerzerolabs/test-devtools-sui": "~0.0.1", + "@layerzerolabs/ua-devtools": "~5.0.2", + "@mysten/sui": "^1.45.2", + "@swc/core": "^1.4.0", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.12", + "fast-check": "^3.15.1", + "jest": "^29.7.0", + "jest-extended": "^4.0.2", + "ts-node": "^10.9.2", + "tslib": "~2.6.2", + "tsup": "~8.0.1", + "typescript": "^5.4.4" + }, + "peerDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-sui": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-sui-oft-sdk-v2": "^3.0.156", + "@layerzerolabs/lz-sui-sdk-v2": "^3.0.156", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@layerzerolabs/protocol-devtools-sui": "~0.1.0", + "@layerzerolabs/ua-devtools": "~5.0.2", + "@mysten/sui": "^1.45.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/ua-devtools-sui/src/index.ts b/packages/ua-devtools-sui/src/index.ts new file mode 100644 index 0000000000..ab18876664 --- /dev/null +++ b/packages/ua-devtools-sui/src/index.ts @@ -0,0 +1 @@ +export * from './oft' diff --git a/packages/ua-devtools-sui/src/oft/config.ts b/packages/ua-devtools-sui/src/oft/config.ts new file mode 100644 index 0000000000..f05c46a4b8 --- /dev/null +++ b/packages/ua-devtools-sui/src/oft/config.ts @@ -0,0 +1,44 @@ +import { + type OmniVector, + type CreateTransactionsFromOmniEdges, + formatOmniVector, + createConfigureEdges, + createConfigureMultiple, + OmniSDKFactory, + OmniPoint, +} from '@layerzerolabs/devtools' +import { createModuleLogger, createWithAsyncLogger } from '@layerzerolabs/io-devtools' +import { isOmniPointOnSui } from '@layerzerolabs/devtools-sui' +import type { IOApp, OAppConfigurator, OAppOmniGraph } from '@layerzerolabs/ua-devtools' +import { OFT } from './sdk' + +const createOFTLogger = () => createModuleLogger('OFT') +const withOFTLogger = createWithAsyncLogger(createOFTLogger) + +const isVectorFromSui = (vector: OmniVector): boolean => isOmniPointOnSui(vector.from) + +const onlyEdgesFromSui = ( + createTransactions: CreateTransactionsFromOmniEdges +): CreateTransactionsFromOmniEdges => { + const logger = createOFTLogger() + + return (edge, sdk, graph, createSdk) => { + if (!isVectorFromSui(edge.vector)) { + return logger.verbose(`Ignoring connection ${formatOmniVector(edge.vector)}`), undefined + } + + return createTransactions(edge, sdk as OFT, graph, createSdk as OmniSDKFactory) + } +} + +export const initConfig: OAppConfigurator = createConfigureEdges( + onlyEdgesFromSui( + withOFTLogger(async () => { + const logger = createOFTLogger() + logger.warn('Sui OFT initConfig is not implemented yet') + return undefined + }) + ) +) + +export const initOFTAccounts = createConfigureMultiple(initConfig) diff --git a/packages/ua-devtools-sui/src/oft/factory.ts b/packages/ua-devtools-sui/src/oft/factory.ts new file mode 100644 index 0000000000..74cef1ed9c --- /dev/null +++ b/packages/ua-devtools-sui/src/oft/factory.ts @@ -0,0 +1,15 @@ +import pMemoize from 'p-memoize' +import type { OAppFactory } from '@layerzerolabs/ua-devtools' +import { OFT } from './sdk' +import { type ConnectionFactory, createConnectionFactory, defaultRpcUrlFactory } from '@layerzerolabs/devtools-sui' + +/** + * Syntactic sugar that creates an instance of Sui `OFT` SDK + * based on an `OmniPoint` with help of an `ConnectionFactory`. + * + * @param {ConnectionFactory} connectionFactory A function that returns a `SuiClient` based on an `EndpointId` + * @returns {OAppFactory} + */ +export const createOFTFactory = ( + connectionFactory: ConnectionFactory = createConnectionFactory(defaultRpcUrlFactory) +): OAppFactory => pMemoize(async (point) => new OFT(await connectionFactory(point.eid), point)) diff --git a/packages/ua-devtools-sui/src/oft/index.ts b/packages/ua-devtools-sui/src/oft/index.ts new file mode 100644 index 0000000000..444d96c1a3 --- /dev/null +++ b/packages/ua-devtools-sui/src/oft/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './factory' +export * from './sdk' diff --git a/packages/ua-devtools-sui/src/oft/sdk.ts b/packages/ua-devtools-sui/src/oft/sdk.ts new file mode 100644 index 0000000000..93261caec1 --- /dev/null +++ b/packages/ua-devtools-sui/src/oft/sdk.ts @@ -0,0 +1,182 @@ +import { Transaction } from '@mysten/sui/transactions' +import type { IOApp, OAppEnforcedOptionParam } from '@layerzerolabs/ua-devtools' +import { endpointIdToStage, Stage, type EndpointId } from '@layerzerolabs/lz-definitions' +import { + areBytes32Equal, + formatEid, + fromHex, + isZero, + toHex, + type Bytes, + type OmniAddress, + type OmniTransaction, +} from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-sui' +import { EndpointV2, SUI_ENDPOINT_V2_ADDRESSES } from '@layerzerolabs/protocol-devtools-sui' +import { SDK } from '@layerzerolabs/lz-sui-sdk-v2' +import type { OApp, Endpoint } from '@layerzerolabs/lz-sui-sdk-v2' + +export class OFT extends OmniSDK implements IOApp { + private sdk?: SDK + private oapp?: OApp + private endpoint?: Endpoint + + async getEndpointSDK(): Promise { + const endpoint = SUI_ENDPOINT_V2_ADDRESSES[this.point.eid] + if (!endpoint) { + throw new Error( + `No Sui EndpointV2 address configured for eid ${this.point.eid} (${formatEid(this.point.eid)})` + ) + } + return new EndpointV2(this.client, { eid: this.point.eid, address: endpoint }) + } + + async getOwner(): Promise { + return this.getDelegate() + } + + async hasOwner(_address: OmniAddress): Promise { + const owner = await this.getOwner() + return owner === _address + } + + async setOwner(_address: OmniAddress): Promise { + return this.setDelegate(_address) + } + + async getPeer(_eid: EndpointId): Promise { + try { + const peer = await this.getOApp().getPeer(_eid) + return isZero(peer) ? undefined : toHex(peer) + } catch (error) { + if (isMissingSuiPeer(error)) { + return undefined + } + throw error + } + } + + async hasPeer(_eid: EndpointId, _address: OmniAddress | null | undefined): Promise { + const peer = await this.getPeer(_eid) + // Use areBytes32Equal for comparison since getPeer returns 32-byte padded addresses + // while _address may be a 20-byte EVM address + return areBytes32Equal(peer, _address) + } + + async setPeer(_eid: EndpointId, _peer: OmniAddress | null | undefined): Promise { + const tx = new Transaction() + // Peer addresses must be 32 bytes (bytes32), so we need to pad EVM addresses (20 bytes) to 32 bytes + let peerBytes: Uint8Array + if (_peer) { + const rawBytes = fromHex(_peer) + if (rawBytes.length === 32) { + peerBytes = rawBytes + } else if (rawBytes.length === 20) { + // Pad EVM address (20 bytes) to 32 bytes with leading zeros + peerBytes = new Uint8Array(32) + peerBytes.set(rawBytes, 32 - rawBytes.length) + } else { + throw new Error(`Invalid peer address length: ${rawBytes.length}. Expected 20 or 32 bytes.`) + } + } else { + peerBytes = new Uint8Array(32) + } + await this.getOApp().setPeerMoveCall(tx, _eid, peerBytes) + return this.createTransaction(tx) + } + + async getDelegate(): Promise { + try { + return this.getEndpoint().getDelegate(this.point.address) + } catch (error) { + if (isMissingSuiPeer(error)) { + return undefined + } + throw error + } + } + + async isDelegate(_address: OmniAddress): Promise { + const delegate = await this.getDelegate() + return delegate === _address + } + + async setDelegate(_address: OmniAddress): Promise { + const tx = new Transaction() + await this.getOApp().setDelegateMoveCall(tx, _address) + return this.createTransaction(tx) + } + + async getEnforcedOptions(_eid: EndpointId, _msgType: number): Promise { + try { + const options = await this.getOApp().getEnforcedOptions(_eid, _msgType) + return toHex(options) + } catch (error) { + if (isMissingSuiPeer(error)) { + return '0x' + } + throw error + } + } + + async setEnforcedOptions(_enforcedOptions: OAppEnforcedOptionParam[]): Promise { + const tx = new Transaction() + for (const { eid, option } of _enforcedOptions) { + await this.getOApp().setEnforcedOptionsMoveCall(tx, eid, option.msgType, fromHex(option.options)) + } + return this.createTransaction(tx) + } + + async getCallerBpsCap(): Promise { + return this.notImplemented('getCallerBpsCap') + } + + async setCallerBpsCap(_callerBpsCap: bigint): Promise { + return this.notImplemented('setCallerBpsCap') + } + + private notImplemented(method: string): never { + throw new TypeError(`${method}() not implemented on Sui OFT SDK`) + } + + private getSdk(): SDK { + if (!this.sdk) { + const stage = endpointIdToStage(this.point.eid) as Stage + this.sdk = new SDK({ client: this.client, stage }) + } + return this.sdk + } + + private getOApp(): OApp { + if (!this.oapp) { + this.oapp = this.getSdk().getOApp(this.point.address) + } + return this.oapp + } + + private getEndpoint(): Endpoint { + if (!this.endpoint) { + this.endpoint = this.getSdk().getEndpoint() + } + return this.endpoint + } +} + +const isMissingSuiPeer = (error: unknown): boolean => { + const message = + typeof error === 'string' + ? error.toLowerCase() + : error && typeof error === 'object' && 'message' in error + ? String((error as { message?: unknown }).message).toLowerCase() + : '' + if (!message) { + return false + } + return ( + message.includes('missing transaction sender') || + (message.includes('move abort') && + // Check for peer/enforced_options patterns in the error message + // The error format is: oapp_peer") ... function_name: Some("get_peer") + (message.includes('oapp_peer') || message.includes('enforced_options'))) + ) +} diff --git a/packages/ua-devtools-sui/tsconfig.json b/packages/ua-devtools-sui/tsconfig.json new file mode 100644 index 0000000000..12774c873d --- /dev/null +++ b/packages/ua-devtools-sui/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["dist", "node_modules"], + "include": ["src", "test", "*.config.ts"], + "compilerOptions": { + "target": "es2020", + "experimentalDecorators": true, + "types": ["node", "jest"], + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/ua-devtools-sui/tsup.config.ts b/packages/ua-devtools-sui/tsup.config.ts new file mode 100644 index 0000000000..7ef46a5ad1 --- /dev/null +++ b/packages/ua-devtools-sui/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + entry: ['src/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, +]) From 5e696efa535c1925c6599235a14631c7ac557384 Mon Sep 17 00:00:00 2001 From: Krak Date: Mon, 26 Jan 2026 14:06:47 -0800 Subject: [PATCH 4/8] feat(devtools-starknet): add Starknet SDK packages for wire support - Add @layerzerolabs/devtools-starknet for Starknet signer/provider - Add @layerzerolabs/ua-devtools-starknet for Starknet OFT SDK - Add @layerzerolabs/protocol-devtools-starknet for EndpointV2/ULN302 SDKs - Fix address comparison in ua-devtools to use areBytes32Equal - Fix library skip logic to prevent SAME_VALUE errors Wire task now works correctly for Starknet OFTs with proper address normalization and idempotent configuration detection. Note: Send FROM Starknet has protocol-level bug in SendLib contract. Send TO Starknet works correctly. --- packages/devtools-starknet/.eslintignore | 2 + packages/devtools-starknet/.eslintrc.json | 3 + packages/devtools-starknet/.prettierignore | 2 + packages/devtools-starknet/CHANGELOG.md | 5 + packages/devtools-starknet/README.md | 9 + packages/devtools-starknet/package.json | 66 +++ .../devtools-starknet/src/common/addresses.ts | 9 + .../devtools-starknet/src/common/index.ts | 1 + .../src/connection/factory.ts | 22 + .../devtools-starknet/src/connection/index.ts | 2 + .../devtools-starknet/src/connection/types.ts | 4 + packages/devtools-starknet/src/index.ts | 4 + .../src/omnigraph/coordinates.ts | 10 + .../devtools-starknet/src/omnigraph/index.ts | 3 + .../devtools-starknet/src/omnigraph/sdk.ts | 24 + .../devtools-starknet/src/omnigraph/types.ts | 3 + .../src/transactions/index.ts | 2 + .../src/transactions/serde.ts | 6 + .../src/transactions/signer.ts | 59 +++ packages/devtools-starknet/tsconfig.json | 8 + packages/devtools-starknet/tsup.config.ts | 9 + .../protocol-devtools-starknet/.eslintignore | 2 + .../protocol-devtools-starknet/.eslintrc.json | 3 + .../.prettierignore | 2 + .../protocol-devtools-starknet/CHANGELOG.md | 5 + packages/protocol-devtools-starknet/README.md | 19 + packages/protocol-devtools-starknet/bin/test | 7 + .../protocol-devtools-starknet/jest.config.js | 14 + .../protocol-devtools-starknet/jest.setup.js | 4 + .../protocol-devtools-starknet/package.json | 79 ++++ .../src/addresses.ts | 16 + .../src/endpointv2/index.ts | 1 + .../src/endpointv2/sdk.ts | 415 ++++++++++++++++++ .../protocol-devtools-starknet/src/index.ts | 4 + .../src/protocol.ts | 66 +++ .../src/uln302/index.ts | 1 + .../src/uln302/sdk.ts | 144 ++++++ .../protocol-devtools-starknet/tsconfig.json | 13 + .../protocol-devtools-starknet/tsup.config.ts | 14 + packages/ua-devtools-starknet/.eslintignore | 2 + packages/ua-devtools-starknet/.eslintrc.json | 3 + packages/ua-devtools-starknet/.prettierignore | 2 + packages/ua-devtools-starknet/CHANGELOG.md | 5 + packages/ua-devtools-starknet/README.md | 19 + packages/ua-devtools-starknet/bin/test | 7 + packages/ua-devtools-starknet/jest.config.js | 14 + packages/ua-devtools-starknet/jest.setup.js | 4 + packages/ua-devtools-starknet/package.json | 83 ++++ packages/ua-devtools-starknet/src/index.ts | 1 + .../ua-devtools-starknet/src/oft/config.ts | 44 ++ .../ua-devtools-starknet/src/oft/factory.ts | 15 + .../ua-devtools-starknet/src/oft/index.ts | 3 + packages/ua-devtools-starknet/src/oft/sdk.ts | 254 +++++++++++ packages/ua-devtools-starknet/tsconfig.json | 13 + packages/ua-devtools-starknet/tsup.config.ts | 14 + packages/ua-devtools/src/oapp/config.ts | 17 +- 56 files changed, 1558 insertions(+), 4 deletions(-) create mode 100644 packages/devtools-starknet/.eslintignore create mode 100644 packages/devtools-starknet/.eslintrc.json create mode 100644 packages/devtools-starknet/.prettierignore create mode 100644 packages/devtools-starknet/CHANGELOG.md create mode 100644 packages/devtools-starknet/README.md create mode 100644 packages/devtools-starknet/package.json create mode 100644 packages/devtools-starknet/src/common/addresses.ts create mode 100644 packages/devtools-starknet/src/common/index.ts create mode 100644 packages/devtools-starknet/src/connection/factory.ts create mode 100644 packages/devtools-starknet/src/connection/index.ts create mode 100644 packages/devtools-starknet/src/connection/types.ts create mode 100644 packages/devtools-starknet/src/index.ts create mode 100644 packages/devtools-starknet/src/omnigraph/coordinates.ts create mode 100644 packages/devtools-starknet/src/omnigraph/index.ts create mode 100644 packages/devtools-starknet/src/omnigraph/sdk.ts create mode 100644 packages/devtools-starknet/src/omnigraph/types.ts create mode 100644 packages/devtools-starknet/src/transactions/index.ts create mode 100644 packages/devtools-starknet/src/transactions/serde.ts create mode 100644 packages/devtools-starknet/src/transactions/signer.ts create mode 100644 packages/devtools-starknet/tsconfig.json create mode 100644 packages/devtools-starknet/tsup.config.ts create mode 100644 packages/protocol-devtools-starknet/.eslintignore create mode 100644 packages/protocol-devtools-starknet/.eslintrc.json create mode 100644 packages/protocol-devtools-starknet/.prettierignore create mode 100644 packages/protocol-devtools-starknet/CHANGELOG.md create mode 100644 packages/protocol-devtools-starknet/README.md create mode 100755 packages/protocol-devtools-starknet/bin/test create mode 100644 packages/protocol-devtools-starknet/jest.config.js create mode 100644 packages/protocol-devtools-starknet/jest.setup.js create mode 100644 packages/protocol-devtools-starknet/package.json create mode 100644 packages/protocol-devtools-starknet/src/addresses.ts create mode 100644 packages/protocol-devtools-starknet/src/endpointv2/index.ts create mode 100644 packages/protocol-devtools-starknet/src/endpointv2/sdk.ts create mode 100644 packages/protocol-devtools-starknet/src/index.ts create mode 100644 packages/protocol-devtools-starknet/src/protocol.ts create mode 100644 packages/protocol-devtools-starknet/src/uln302/index.ts create mode 100644 packages/protocol-devtools-starknet/src/uln302/sdk.ts create mode 100644 packages/protocol-devtools-starknet/tsconfig.json create mode 100644 packages/protocol-devtools-starknet/tsup.config.ts create mode 100644 packages/ua-devtools-starknet/.eslintignore create mode 100644 packages/ua-devtools-starknet/.eslintrc.json create mode 100644 packages/ua-devtools-starknet/.prettierignore create mode 100644 packages/ua-devtools-starknet/CHANGELOG.md create mode 100644 packages/ua-devtools-starknet/README.md create mode 100755 packages/ua-devtools-starknet/bin/test create mode 100644 packages/ua-devtools-starknet/jest.config.js create mode 100644 packages/ua-devtools-starknet/jest.setup.js create mode 100644 packages/ua-devtools-starknet/package.json create mode 100644 packages/ua-devtools-starknet/src/index.ts create mode 100644 packages/ua-devtools-starknet/src/oft/config.ts create mode 100644 packages/ua-devtools-starknet/src/oft/factory.ts create mode 100644 packages/ua-devtools-starknet/src/oft/index.ts create mode 100644 packages/ua-devtools-starknet/src/oft/sdk.ts create mode 100644 packages/ua-devtools-starknet/tsconfig.json create mode 100644 packages/ua-devtools-starknet/tsup.config.ts diff --git a/packages/devtools-starknet/.eslintignore b/packages/devtools-starknet/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/packages/devtools-starknet/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/devtools-starknet/.eslintrc.json b/packages/devtools-starknet/.eslintrc.json new file mode 100644 index 0000000000..be97c53fbb --- /dev/null +++ b/packages/devtools-starknet/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/devtools-starknet/.prettierignore b/packages/devtools-starknet/.prettierignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/packages/devtools-starknet/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/packages/devtools-starknet/CHANGELOG.md b/packages/devtools-starknet/CHANGELOG.md new file mode 100644 index 0000000000..bd25c28af7 --- /dev/null +++ b/packages/devtools-starknet/CHANGELOG.md @@ -0,0 +1,5 @@ +# @layerzerolabs/devtools-starknet + +## 0.1.0 + +- Initial release. diff --git a/packages/devtools-starknet/README.md b/packages/devtools-starknet/README.md new file mode 100644 index 0000000000..b05cf14341 --- /dev/null +++ b/packages/devtools-starknet/README.md @@ -0,0 +1,9 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/devtools-starknet

+ +Utilities for working with LayerZero Starknet contracts. diff --git a/packages/devtools-starknet/package.json b/packages/devtools-starknet/package.json new file mode 100644 index 0000000000..6d6290130a --- /dev/null +++ b/packages/devtools-starknet/package.json @@ -0,0 +1,66 @@ +{ + "name": "@layerzerolabs/devtools-starknet", + "version": "0.1.0", + "description": "Developer utilities for working with LayerZero Starknet contracts", + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/devtools.git", + "directory": "packages/devtools-starknet" + }, + "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": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/protocol-starknet-v2": "^0.2.19", + "@swc/core": "^1.4.0", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.12", + "jest": "^29.7.0", + "starknet": "^8.9.0", + "ts-node": "^10.9.2", + "tslib": "~2.6.2", + "tsup": "~8.0.1", + "typescript": "^5.4.4" + }, + "peerDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "starknet": "^8.9.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/devtools-starknet/src/common/addresses.ts b/packages/devtools-starknet/src/common/addresses.ts new file mode 100644 index 0000000000..2beeee2845 --- /dev/null +++ b/packages/devtools-starknet/src/common/addresses.ts @@ -0,0 +1,9 @@ +const STARKNET_HEX_REGEX = /^0x[0-9a-fA-F]{1,64}$/ + +export const isStarknetAddress = (value: string): boolean => STARKNET_HEX_REGEX.test(value) + +export const assertStarknetAddress = (value: string, label = 'address'): void => { + if (!isStarknetAddress(value)) { + throw new Error(`Invalid Starknet ${label}: ${value}`) + } +} diff --git a/packages/devtools-starknet/src/common/index.ts b/packages/devtools-starknet/src/common/index.ts new file mode 100644 index 0000000000..897b610db2 --- /dev/null +++ b/packages/devtools-starknet/src/common/index.ts @@ -0,0 +1 @@ +export * from './addresses' diff --git a/packages/devtools-starknet/src/connection/factory.ts b/packages/devtools-starknet/src/connection/factory.ts new file mode 100644 index 0000000000..f99988bdac --- /dev/null +++ b/packages/devtools-starknet/src/connection/factory.ts @@ -0,0 +1,22 @@ +import pMemoize from 'p-memoize' +import { type RpcUrlFactory } from '@layerzerolabs/devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import { RpcProvider } from 'starknet' +import type { ConnectionFactory } from './types' + +export const defaultRpcUrlFactory: RpcUrlFactory = (eid) => { + throw new Error( + `No default Starknet RPC URL configured for eid ${eid}. Provide an override via createRpcUrlFactory().` + ) +} + +export const createRpcUrlFactory = + (overrides: Partial> = {}): RpcUrlFactory => + (eid) => + overrides[eid] ?? + process.env.RPC_URL_STARKNET ?? + process.env.RPC_URL_STARKNET_TESTNET ?? + defaultRpcUrlFactory(eid) + +export const createConnectionFactory = (urlFactory: RpcUrlFactory = defaultRpcUrlFactory): ConnectionFactory => + pMemoize(async (eid) => new RpcProvider({ nodeUrl: await urlFactory(eid) })) diff --git a/packages/devtools-starknet/src/connection/index.ts b/packages/devtools-starknet/src/connection/index.ts new file mode 100644 index 0000000000..97a7b59914 --- /dev/null +++ b/packages/devtools-starknet/src/connection/index.ts @@ -0,0 +1,2 @@ +export * from './factory' +export * from './types' diff --git a/packages/devtools-starknet/src/connection/types.ts b/packages/devtools-starknet/src/connection/types.ts new file mode 100644 index 0000000000..4e19316a6c --- /dev/null +++ b/packages/devtools-starknet/src/connection/types.ts @@ -0,0 +1,4 @@ +import type { RpcProvider } from 'starknet' +import type { EndpointId } from '@layerzerolabs/lz-definitions' + +export type ConnectionFactory = (eid: EndpointId) => Promise diff --git a/packages/devtools-starknet/src/index.ts b/packages/devtools-starknet/src/index.ts new file mode 100644 index 0000000000..ded5780815 --- /dev/null +++ b/packages/devtools-starknet/src/index.ts @@ -0,0 +1,4 @@ +export * from './common' +export * from './connection' +export * from './omnigraph' +export * from './transactions' diff --git a/packages/devtools-starknet/src/omnigraph/coordinates.ts b/packages/devtools-starknet/src/omnigraph/coordinates.ts new file mode 100644 index 0000000000..0742758b51 --- /dev/null +++ b/packages/devtools-starknet/src/omnigraph/coordinates.ts @@ -0,0 +1,10 @@ +import type { OmniPoint } from '@layerzerolabs/devtools' +import { ChainType, endpointIdToChainType, type EndpointId } from '@layerzerolabs/lz-definitions' +import { assertStarknetAddress } from '../common' + +export const createStarknetPoint = (eid: EndpointId, address: string): OmniPoint => { + assertStarknetAddress(address) + return { eid, address } +} + +export const isOmniPointOnStarknet = ({ eid }: OmniPoint): boolean => endpointIdToChainType(eid) === ChainType.STARKNET diff --git a/packages/devtools-starknet/src/omnigraph/index.ts b/packages/devtools-starknet/src/omnigraph/index.ts new file mode 100644 index 0000000000..02b48ea20f --- /dev/null +++ b/packages/devtools-starknet/src/omnigraph/index.ts @@ -0,0 +1,3 @@ +export * from './coordinates' +export * from './sdk' +export * from './types' diff --git a/packages/devtools-starknet/src/omnigraph/sdk.ts b/packages/devtools-starknet/src/omnigraph/sdk.ts new file mode 100644 index 0000000000..29ba4795a2 --- /dev/null +++ b/packages/devtools-starknet/src/omnigraph/sdk.ts @@ -0,0 +1,24 @@ +import type { Call, RpcProvider } from 'starknet' +import { formatOmniPoint, OmniPoint, OmniTransaction } from '@layerzerolabs/devtools' +import { createModuleLogger, type Logger } from '@layerzerolabs/io-devtools' +import type { IOmniSDK } from './types' +import { serializeStarknetCalls } from '../transactions' + +export class OmniSDK implements IOmniSDK { + constructor( + public readonly provider: RpcProvider, + public readonly point: OmniPoint, + protected readonly logger: Logger = createModuleLogger(`Starknet SDK @ ${formatOmniPoint(point)}`) + ) {} + + get label(): string { + return `Starknet contract @ ${formatOmniPoint(this.point)}` + } + + protected createTransaction(calls: Call[]): OmniTransaction { + return { + point: this.point, + data: serializeStarknetCalls(calls), + } + } +} diff --git a/packages/devtools-starknet/src/omnigraph/types.ts b/packages/devtools-starknet/src/omnigraph/types.ts new file mode 100644 index 0000000000..4f75ffaa06 --- /dev/null +++ b/packages/devtools-starknet/src/omnigraph/types.ts @@ -0,0 +1,3 @@ +import type { IOmniSDK as IOmniSDKAbstract } from '@layerzerolabs/devtools' + +export interface IOmniSDK extends IOmniSDKAbstract {} diff --git a/packages/devtools-starknet/src/transactions/index.ts b/packages/devtools-starknet/src/transactions/index.ts new file mode 100644 index 0000000000..18aee9d5ff --- /dev/null +++ b/packages/devtools-starknet/src/transactions/index.ts @@ -0,0 +1,2 @@ +export * from './serde' +export * from './signer' diff --git a/packages/devtools-starknet/src/transactions/serde.ts b/packages/devtools-starknet/src/transactions/serde.ts new file mode 100644 index 0000000000..43933f5cc2 --- /dev/null +++ b/packages/devtools-starknet/src/transactions/serde.ts @@ -0,0 +1,6 @@ +import type { Call } from 'starknet' + +export const serializeStarknetCalls = (calls: Call[]): string => + JSON.stringify(calls, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)) + +export const deserializeStarknetCalls = (data: string): Call[] => JSON.parse(data) as Call[] diff --git a/packages/devtools-starknet/src/transactions/signer.ts b/packages/devtools-starknet/src/transactions/signer.ts new file mode 100644 index 0000000000..cad0e74189 --- /dev/null +++ b/packages/devtools-starknet/src/transactions/signer.ts @@ -0,0 +1,59 @@ +import type { Account, Call, RpcProvider } from 'starknet' +import { + OmniSigner, + OmniSignerBase, + type OmniSignerFactory, + type OmniTransaction, + type OmniTransactionReceipt, + type OmniTransactionResponse, + OmniPoint, +} from '@layerzerolabs/devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import { deserializeStarknetCalls } from './serde' +import { createModuleLogger, type Logger } from '@layerzerolabs/io-devtools' + +export class OmniSignerStarknet extends OmniSignerBase implements OmniSigner { + constructor( + eid: EndpointId, + public readonly provider: RpcProvider, + public readonly account: Account, + protected readonly logger: Logger = createModuleLogger('OmniSignerStarknet') + ) { + super(eid) + } + + getPoint(): OmniPoint { + return { eid: this.eid, address: this.account.address } + } + + async sign(): Promise { + throw new Error('Starknet OmniSigner does not support offline signing. Use signAndSend instead.') + } + + async signAndSend(transaction: OmniTransaction): Promise> { + this.assertTransaction(transaction) + + const calls = deserializeStarknetCalls(transaction.data) as Call[] + const response = await this.account.execute(calls) + const transactionHash = response.transaction_hash + + return { + transactionHash, + wait: async () => { + await this.provider.waitForTransaction(transactionHash) + return { transactionHash } + }, + } + } +} + +export type StarknetAccountFactory = (eid: EndpointId) => Promise +export type StarknetProviderFactory = (eid: EndpointId) => Promise + +export const createStarknetSignerFactory = + ( + accountFactory: StarknetAccountFactory, + providerFactory: StarknetProviderFactory + ): OmniSignerFactory>> => + async (eid: EndpointId) => + new OmniSignerStarknet(eid, await providerFactory(eid), await accountFactory(eid)) diff --git a/packages/devtools-starknet/tsconfig.json b/packages/devtools-starknet/tsconfig.json new file mode 100644 index 0000000000..75eba9f992 --- /dev/null +++ b/packages/devtools-starknet/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/devtools-starknet/tsup.config.ts b/packages/devtools-starknet/tsup.config.ts new file mode 100644 index 0000000000..cbe50c7c92 --- /dev/null +++ b/packages/devtools-starknet/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + sourcemap: true, + clean: true, +}) diff --git a/packages/protocol-devtools-starknet/.eslintignore b/packages/protocol-devtools-starknet/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/packages/protocol-devtools-starknet/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/protocol-devtools-starknet/.eslintrc.json b/packages/protocol-devtools-starknet/.eslintrc.json new file mode 100644 index 0000000000..be97c53fbb --- /dev/null +++ b/packages/protocol-devtools-starknet/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/protocol-devtools-starknet/.prettierignore b/packages/protocol-devtools-starknet/.prettierignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/packages/protocol-devtools-starknet/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/packages/protocol-devtools-starknet/CHANGELOG.md b/packages/protocol-devtools-starknet/CHANGELOG.md new file mode 100644 index 0000000000..1181690e98 --- /dev/null +++ b/packages/protocol-devtools-starknet/CHANGELOG.md @@ -0,0 +1,5 @@ +# @layerzerolabs/protocol-devtools-starknet + +## 0.1.0 + +- Initial release. diff --git a/packages/protocol-devtools-starknet/README.md b/packages/protocol-devtools-starknet/README.md new file mode 100644 index 0000000000..6474cd79a7 --- /dev/null +++ b/packages/protocol-devtools-starknet/README.md @@ -0,0 +1,19 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/protocol-devtools-starknet

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +Utilities for working with LayerZero Starknet protocol contracts. diff --git a/packages/protocol-devtools-starknet/bin/test b/packages/protocol-devtools-starknet/bin/test new file mode 100755 index 0000000000..cb0085aeda --- /dev/null +++ b/packages/protocol-devtools-starknet/bin/test @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ -z "${LZ_DEVTOOLS_ENABLE_STARKNET_TESTS}" ]; then + echo 'Starknet tests can be enabled by setting LZ_DEVTOOLS_ENABLE_STARKNET_TESTS environment variable to a non-empty value' +else + jest --ci "$@" +fi diff --git a/packages/protocol-devtools-starknet/jest.config.js b/packages/protocol-devtools-starknet/jest.config.js new file mode 100644 index 0000000000..04c8477342 --- /dev/null +++ b/packages/protocol-devtools-starknet/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + cache: false, + reporters: [['github-actions', { silent: false }], 'default'], + testEnvironment: 'node', + testTimeout: 60_000, + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, +}; diff --git a/packages/protocol-devtools-starknet/jest.setup.js b/packages/protocol-devtools-starknet/jest.setup.js new file mode 100644 index 0000000000..956faa4d5c --- /dev/null +++ b/packages/protocol-devtools-starknet/jest.setup.js @@ -0,0 +1,4 @@ +import * as jestExtended from 'jest-extended'; + +// add all jest-extended matchers +expect.extend(jestExtended); diff --git a/packages/protocol-devtools-starknet/package.json b/packages/protocol-devtools-starknet/package.json new file mode 100644 index 0000000000..f3ca137da7 --- /dev/null +++ b/packages/protocol-devtools-starknet/package.json @@ -0,0 +1,79 @@ +{ + "name": "@layerzerolabs/protocol-devtools-starknet", + "version": "0.1.0", + "description": "Utilities for LayerZero Starknet protocol contracts", + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/devtools.git", + "directory": "packages/protocol-devtools-starknet" + }, + "license": "MIT", + "sideEffects": false, + "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": "$npm_execpath tsc --noEmit", + "build": "$npm_execpath tsup", + "clean": "rm -rf dist", + "dev": "$npm_execpath tsup --watch", + "lint": "$npm_execpath eslint '**/*.{js,ts,json}'", + "lint:fix": "eslint --fix '**/*.{js,ts,json}'", + "test": "./bin/test" + }, + "dependencies": { + "p-memoize": "~4.0.4" + }, + "devDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-starknet": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@layerzerolabs/protocol-starknet-v2": "^0.2.19", + "@layerzerolabs/test-devtools": "~0.4.7", + "@layerzerolabs/test-devtools-starknet": "~0.0.1", + "@swc/core": "^1.4.0", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.12", + "fast-check": "^3.15.1", + "jest": "^29.7.0", + "jest-extended": "^4.0.2", + "starknet": "^8.9.0", + "ts-node": "^10.9.2", + "tslib": "~2.6.2", + "tsup": "~8.0.1", + "typescript": "^5.4.4", + "zod": "^3.22.4" + }, + "peerDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-starknet": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@layerzerolabs/protocol-starknet-v2": "^0.2.19", + "starknet": "^8.9.0", + "zod": "^3.22.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/protocol-devtools-starknet/src/addresses.ts b/packages/protocol-devtools-starknet/src/addresses.ts new file mode 100644 index 0000000000..1887a15db4 --- /dev/null +++ b/packages/protocol-devtools-starknet/src/addresses.ts @@ -0,0 +1,16 @@ +import { EndpointId } from '@layerzerolabs/lz-definitions' + +export const STARKNET_ENDPOINT_V2_ADDRESSES: Partial> = { + [EndpointId.STARKNET_V2_MAINNET]: '0x0524e065abff21d225fb7b28f26ec2f48314ace6094bc085f0a7cf1dc2660f68', + [EndpointId.STARKNET_V2_TESTNET]: '0x0316d70a6e0445a58c486215fac8ead48d3db985acde27efca9130da4c675878', +} + +export const STARKNET_ULN_302_ADDRESSES: Partial> = { + [EndpointId.STARKNET_V2_MAINNET]: '0x0727f40349719ac76861a51a0b3d3e07be1577fff137bb81a5dc32e5a5c61d38', + [EndpointId.STARKNET_V2_TESTNET]: '0x0706572d6f7b938c813a20dc1b0328b83de939066e25bd0fbe14c270077f769d', +} + +export const STARKNET_EXECUTOR_ADDRESSES: Partial> = { + [EndpointId.STARKNET_V2_MAINNET]: '0x03887bd8da2999d39e2e88fe55733c4cac8e20a6d51bfe162176c9f2eb134c65', + [EndpointId.STARKNET_V2_TESTNET]: '0x068ffdaca6533001344f377beaf1137360168604b227df3e8cf735fe06da47a9', +} diff --git a/packages/protocol-devtools-starknet/src/endpointv2/index.ts b/packages/protocol-devtools-starknet/src/endpointv2/index.ts new file mode 100644 index 0000000000..7db67b18a1 --- /dev/null +++ b/packages/protocol-devtools-starknet/src/endpointv2/index.ts @@ -0,0 +1 @@ +export * from './sdk' diff --git a/packages/protocol-devtools-starknet/src/endpointv2/sdk.ts b/packages/protocol-devtools-starknet/src/endpointv2/sdk.ts new file mode 100644 index 0000000000..01744035ab --- /dev/null +++ b/packages/protocol-devtools-starknet/src/endpointv2/sdk.ts @@ -0,0 +1,415 @@ +import type { + IEndpointV2, + IUlnRead, + MessageParams, + MessagingFee, + SetConfigParam, + Timeout, + Uln302ConfigType, + Uln302ExecutorConfig, + Uln302SetExecutorConfig, + Uln302SetUlnConfig, + Uln302UlnConfig, + Uln302UlnUserConfig, + UlnReadSetUlnConfig, + UlnReadUlnConfig, + UlnReadUlnUserConfig, +} from '@layerzerolabs/protocol-devtools' +import { type EndpointId } from '@layerzerolabs/lz-definitions' +import { + formatEid, + type Bytes32, + type OmniAddress, + type OmniTransaction, + type PossiblyBytes, +} from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-starknet' +import type { Call, Contract } from 'starknet' +import type { Uln302 } from '../uln302' +import { STARKNET_ENDPOINT_V2_ADDRESSES } from '../addresses' +import { + encodeExecutorConfig, + encodeUlnConfig, + getEndpointV2Contract, + getOAppContract, + MessageLibConfigType, +} from '../protocol' + +const RECEIVE_ULN_CONFIG_TYPE = 3 + +export class EndpointV2 extends OmniSDK implements IEndpointV2 { + private endpoint?: Contract + + async getUln302SDK(address: OmniAddress): Promise { + this.logger.debug(`Getting Uln302 SDK for address ${address}`) + const { Uln302 } = await import('../uln302') + return new Uln302(this.provider, { eid: this.point.eid, address }) + } + + async getUlnReadSDK(_address: OmniAddress): Promise { + throw new Error('ULN Read functionality is not supported for Starknet.') + } + + async getDelegate(_oapp: OmniAddress): Promise { + const oapp = await this.getOApp(_oapp) + if (!('get_delegate' in oapp)) { + return this.notImplemented('getDelegate') + } + const result = await (oapp as any).get_delegate() + return this.parseFelt(result) + } + + async isDelegate(_oapp: OmniAddress, _delegate: OmniAddress): Promise { + const delegate = await this.getDelegate(_oapp) + return delegate === _delegate + } + + async getDefaultReceiveLibrary(_eid: EndpointId): Promise { + const endpoint = await this.getEndpoint() + const result = await (endpoint as any).get_default_receive_library(_eid) + return this.parseFelt(result) + } + + async setDefaultReceiveLibrary( + _eid: EndpointId, + _uln: OmniAddress | null | undefined, + _gracePeriod: bigint = 0n + ): Promise { + return this.notImplemented('setDefaultReceiveLibrary') + } + + async getDefaultSendLibrary(_eid: EndpointId): Promise { + const endpoint = await this.getEndpoint() + const result = await (endpoint as any).get_default_send_library(_eid) + return this.parseFelt(result) + } + + async setDefaultSendLibrary(_eid: EndpointId, _uln: OmniAddress | null | undefined): Promise { + return this.notImplemented('setDefaultSendLibrary') + } + + async isRegisteredLibrary(_uln: OmniAddress): Promise { + return this.notImplemented('isRegisteredLibrary') + } + + async registerLibrary(_uln: OmniAddress): Promise { + return this.notImplemented('registerLibrary') + } + + async isBlockedLibrary(_uln: OmniAddress): Promise { + return this.notImplemented('isBlockedLibrary') + } + + async getSendLibrary(_sender: OmniAddress, _dstEid: EndpointId): Promise { + const endpoint = await this.getEndpoint() + const result = await (endpoint as any).get_send_library(_sender, _dstEid) + return this.parseFelt(result?.lib ?? result) + } + + async getReceiveLibrary( + _receiver: OmniAddress, + _srcEid: EndpointId + ): Promise<[address: Bytes32 | undefined, isDefault: boolean]> { + const endpoint = await this.getEndpoint() + const result = await (endpoint as any).get_receive_library(_receiver, _srcEid) + const address = this.parseFelt(result?.lib ?? result) + const isDefault = Boolean(result?.is_default) + return [address, isDefault] + } + + async getDefaultReceiveLibraryTimeout(_eid: EndpointId): Promise { + return this.notImplemented('getDefaultReceiveLibraryTimeout') + } + + async getReceiveLibraryTimeout(_receiver: OmniAddress, _srcEid: EndpointId): Promise { + return this.notImplemented('getReceiveLibraryTimeout') + } + + async setSendLibrary(_oapp: OmniAddress, _eid: EndpointId, _uln: OmniAddress): Promise { + const endpoint = await this.getEndpoint() + const call = (endpoint as any).populateTransaction.set_send_library(_oapp, _eid, _uln) + return this.createTransactionWithDescription([call], `Setting send library for ${formatEid(_eid)} to ${_uln}`) + } + + async isDefaultSendLibrary(_sender: PossiblyBytes, _dstEid: EndpointId): Promise { + // Get the send library for this sender and destination + const sendLib = await this.getSendLibrary(String(_sender), _dstEid) + // Get the default send library for this destination + const defaultLib = await this.getDefaultSendLibrary(_dstEid) + // If the send library matches the default, or if no specific send library is set, it's using the default + return sendLib === defaultLib || sendLib == null + } + + async setReceiveLibrary( + _oapp: OmniAddress, + _eid: EndpointId, + _uln: OmniAddress, + _gracePeriod: bigint + ): Promise { + const endpoint = await this.getEndpoint() + const call = (endpoint as any).populateTransaction.set_receive_library(_oapp, _eid, _uln, _gracePeriod) + return this.createTransactionWithDescription( + [call], + `Setting receive library for ${formatEid(_eid)} to ${_uln}` + ) + } + + async setReceiveLibraryTimeout( + _oapp: OmniAddress, + _eid: EndpointId, + _uln: OmniAddress, + _expiry: bigint + ): Promise { + return this.notImplemented('setReceiveLibraryTimeout') + } + + async getExecutorConfig(_oapp: PossiblyBytes, _uln: OmniAddress, _eid: EndpointId): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getExecutorConfig(_eid, String(_oapp)) + } + + async getAppExecutorConfig( + _oapp: PossiblyBytes, + _uln: OmniAddress, + _eid: EndpointId + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getAppExecutorConfig(_eid, String(_oapp)) + } + + async hasAppExecutorConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _config: Uln302ExecutorConfig + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.hasAppExecutorConfig(_eid, _oapp, _config) + } + + async setExecutorConfig( + _oapp: PossiblyBytes, + _uln: PossiblyBytes, + _setExecutorConfig: Uln302SetExecutorConfig[] + ): Promise { + const endpoint = await this.getEndpoint() + return _setExecutorConfig.map(({ eid, executorConfig }) => { + const encoded = encodeExecutorConfig({ + max_message_size: executorConfig.maxMessageSize, + executor: executorConfig.executor, + }) + const call = (endpoint as any).populateTransaction.set_send_configs(String(_oapp), String(_uln), [ + { + eid, + config_type: MessageLibConfigType.EXECUTOR, + config: encoded, + }, + ]) + return this.createTransactionWithDescription([call], `Setting executor config for ${formatEid(eid)}`) + }) + } + + async getUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _type: Uln302ConfigType + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getUlnConfig(_eid, _oapp, _type) + } + + async getAppUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _type: Uln302ConfigType + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.getAppUlnConfig(_eid, _oapp, _type) + } + + async getAppUlnReadConfig(_oapp: OmniAddress, _uln: OmniAddress, _channelId: number): Promise { + throw new Error('ULN Read functionality is not supported for Starknet.') + } + + async hasAppUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _eid: EndpointId, + _config: Uln302UlnUserConfig, + _type: Uln302ConfigType + ): Promise { + const ulnSdk = await this.getUln302SDK(_uln) + return ulnSdk.hasAppUlnConfig(_eid, _oapp, _config, _type) + } + + async hasAppUlnReadConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _channelId: number, + _config: UlnReadUlnUserConfig + ): Promise { + throw new Error('ULN Read functionality is not supported for Starknet.') + } + + async setUlnConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _setUlnConfig: Uln302SetUlnConfig[] + ): Promise { + const endpoint = await this.getEndpoint() + return _setUlnConfig.map(({ eid, ulnConfig, type }) => { + const encoded = encodeUlnConfig({ + confirmations: ulnConfig.confirmations ?? 0n, + required_dvns: ulnConfig.requiredDVNs, + optional_dvns: ulnConfig.optionalDVNs ?? [], + optional_dvn_threshold: ulnConfig.optionalDVNThreshold ?? 0, + }) + const call = + type === 'send' + ? (endpoint as any).populateTransaction.set_send_configs(_oapp, _uln, [ + { + eid, + config_type: MessageLibConfigType.ULN, + config: encoded, + }, + ]) + : (endpoint as any).populateTransaction.set_receive_configs(_oapp, _uln, [ + { + eid, + config_type: MessageLibConfigType.ULN, + config: encoded, + }, + ]) + return this.createTransactionWithDescription([call], `Setting ${type} ULN config for ${formatEid(eid)}`) + }) + } + + async setUlnReadConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _setUlnConfig: UlnReadSetUlnConfig[] + ): Promise { + throw new Error('ULN Read functionality is not supported for Starknet.') + } + + async getUlnConfigParams(_uln: OmniAddress, _setUlnConfig: Uln302SetUlnConfig[]): Promise { + return _setUlnConfig.map(({ eid, ulnConfig, type }) => { + return { + eid, + configType: type === 'send' ? MessageLibConfigType.ULN : RECEIVE_ULN_CONFIG_TYPE, + config: encodeUlnConfig({ + confirmations: ulnConfig.confirmations ?? 0n, + required_dvns: ulnConfig.requiredDVNs, + optional_dvns: ulnConfig.optionalDVNs ?? [], + optional_dvn_threshold: ulnConfig.optionalDVNThreshold ?? 0, + }), + } + }) + } + + async getUlnReadConfigParams(_uln: OmniAddress, _setUlnConfig: UlnReadSetUlnConfig[]): Promise { + throw new Error('ULN Read functionality is not supported for Starknet.') + } + + async getExecutorConfigParams( + _uln: OmniAddress, + _setExecutorConfig: Uln302SetExecutorConfig[] + ): Promise { + return _setExecutorConfig.map(({ eid, executorConfig }) => ({ + eid, + configType: MessageLibConfigType.EXECUTOR, + config: encodeExecutorConfig({ + max_message_size: executorConfig.maxMessageSize, + executor: executorConfig.executor, + }), + })) + } + + async setConfig( + _oapp: OmniAddress, + _uln: OmniAddress, + _setConfigParam: SetConfigParam[] + ): Promise { + const endpoint = await this.getEndpoint() + return _setConfigParam.map(({ eid, configType, config }) => { + if (configType === RECEIVE_ULN_CONFIG_TYPE) { + const call = (endpoint as any).populateTransaction.set_receive_configs(_oapp, _uln, [ + { + eid, + config_type: MessageLibConfigType.ULN, + config, + }, + ]) + return this.createTransactionWithDescription([call], `Setting receive ULN config for ${formatEid(eid)}`) + } + + const call = (endpoint as any).populateTransaction.set_send_configs(_oapp, _uln, [ + { + eid, + config_type: configType, + config, + }, + ]) + return this.createTransactionWithDescription( + [call], + `Setting send config ${configType} for ${formatEid(eid)}` + ) + }) + } + + async quote(_params: MessageParams, _sender: OmniAddress): Promise { + return this.notImplemented('quote') + } + + private notImplemented(method: string): never { + throw new TypeError(`${method}() not implemented on Starknet Endpoint SDK`) + } + + private async getEndpoint(): Promise { + if (!this.endpoint) { + const endpointAddress = STARKNET_ENDPOINT_V2_ADDRESSES[this.point.eid] + if (!endpointAddress) { + throw new Error(`Missing Starknet EndpointV2 address for eid ${this.point.eid}`) + } + this.endpoint = getEndpointV2Contract(endpointAddress, this.provider) + } + return this.endpoint! + } + + private async getOApp(address: OmniAddress): Promise { + return getOAppContract(address, this.provider) + } + + protected override createTransaction(calls: Call[]): OmniTransaction { + return super.createTransaction(calls) + } + + private createTransactionWithDescription(calls: Call[], description: string): OmniTransaction { + return { ...super.createTransaction(calls), description } + } + + private parseFelt(value: unknown): string | undefined { + if (value == null) { + return undefined + } + let hexValue: string | undefined + if (typeof value === 'string') { + hexValue = value + } else if (typeof value === 'bigint') { + hexValue = `0x${value.toString(16)}` + } else if (typeof value === 'object' && value !== null && 'value' in value) { + const feltValue = (value as { value: bigint | string }).value + hexValue = typeof feltValue === 'bigint' ? `0x${feltValue.toString(16)}` : String(feltValue) + } + // Normalize Starknet felt252 addresses by removing leading zeros after 0x + // This ensures consistent comparison regardless of how addresses are formatted + // e.g., 0x0727f... and 0x727f... should be treated as the same address + if (hexValue) { + const normalized = hexValue.toLowerCase().replace(/^0x0*/, '0x') + return normalized === '0x' ? '0x0' : normalized + } + return undefined + } +} diff --git a/packages/protocol-devtools-starknet/src/index.ts b/packages/protocol-devtools-starknet/src/index.ts new file mode 100644 index 0000000000..8571ea73f9 --- /dev/null +++ b/packages/protocol-devtools-starknet/src/index.ts @@ -0,0 +1,4 @@ +export * from './addresses' +export * from './protocol' +export * from './endpointv2' +export * from './uln302' diff --git a/packages/protocol-devtools-starknet/src/protocol.ts b/packages/protocol-devtools-starknet/src/protocol.ts new file mode 100644 index 0000000000..9a088a2f3f --- /dev/null +++ b/packages/protocol-devtools-starknet/src/protocol.ts @@ -0,0 +1,66 @@ +import { Contract } from 'starknet' +import { abi } from '@layerzerolabs/protocol-starknet-v2' + +type StarknetProvider = unknown + +export enum MessageLibConfigType { + EXECUTOR = 1, + ULN = 2, +} + +type ExecutorConfigInput = { + max_message_size?: number + maxMessageSize?: number + executor: string +} + +type UlnConfigInput = { + confirmations?: bigint | number + required_dvns?: string[] + requiredDVNs?: string[] + optional_dvns?: string[] + optionalDVNs?: string[] + optional_dvn_threshold?: number + optionalDVNThreshold?: number +} + +const toBoolFelt = (value: boolean) => (value ? 1 : 0) + +export const encodeExecutorConfig = (config: ExecutorConfigInput): (string | number | bigint)[] => { + const maxMessageSize = config.max_message_size ?? config.maxMessageSize ?? 0 + return [maxMessageSize, config.executor] +} + +export const encodeUlnConfig = (config: UlnConfigInput): (string | number | bigint)[] => { + const confirmations = BigInt(config.confirmations ?? 0) + const requiredDvns = config.required_dvns ?? config.requiredDVNs ?? [] + const optionalDvns = config.optional_dvns ?? config.optionalDVNs ?? [] + const optionalThreshold = config.optional_dvn_threshold ?? config.optionalDVNThreshold ?? 0 + const hasConfirmations = confirmations !== 0n + const hasRequiredDvns = requiredDvns.length > 0 + const hasOptionalDvns = optionalDvns.length > 0 + + return [ + confirmations, + toBoolFelt(hasConfirmations), + requiredDvns.length, + ...requiredDvns, + toBoolFelt(hasRequiredDvns), + optionalDvns.length, + ...optionalDvns, + optionalThreshold, + toBoolFelt(hasOptionalDvns), + ] +} + +const createContract = (contractAbi: unknown, address: string, provider: StarknetProvider) => + new (Contract as any)({ abi: contractAbi, address, providerOrAccount: provider }) + +export const getEndpointV2Contract = (address: string, provider: StarknetProvider) => + createContract(abi.endpointV2, address, provider) + +export const getOAppContract = (address: string, provider: StarknetProvider) => + createContract(abi.oApp, address, provider) + +export const getUltraLightNodeContractWithAddress = (address: string, provider: StarknetProvider) => + createContract(abi.ultraLightNode302, address, provider) diff --git a/packages/protocol-devtools-starknet/src/uln302/index.ts b/packages/protocol-devtools-starknet/src/uln302/index.ts new file mode 100644 index 0000000000..7db67b18a1 --- /dev/null +++ b/packages/protocol-devtools-starknet/src/uln302/index.ts @@ -0,0 +1 @@ +export * from './sdk' diff --git a/packages/protocol-devtools-starknet/src/uln302/sdk.ts b/packages/protocol-devtools-starknet/src/uln302/sdk.ts new file mode 100644 index 0000000000..9fed972057 --- /dev/null +++ b/packages/protocol-devtools-starknet/src/uln302/sdk.ts @@ -0,0 +1,144 @@ +import type { + IUln302, + Uln302ConfigType, + Uln302ExecutorConfig, + Uln302UlnConfig, + Uln302UlnUserConfig, +} from '@layerzerolabs/protocol-devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import type { OmniAddress, OmniTransaction } from '@layerzerolabs/devtools' +import { areBytes32Equal } from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-starknet' +import type { Contract } from 'starknet' +import { getUltraLightNodeContractWithAddress } from '../protocol' + +export class Uln302 extends OmniSDK implements IUln302 { + private uln?: Contract + + async getUlnConfig( + _eid: EndpointId, + _address: OmniAddress | null | undefined, + _type: Uln302ConfigType + ): Promise { + if (!_address) { + return this.emptyUlnConfig() + } + return this.getAppUlnConfig(_eid, _address, _type) + } + + async getAppUlnConfig(_eid: EndpointId, _address: OmniAddress, _type: Uln302ConfigType): Promise { + const uln = await this.getUln() + const config = + _type === 'send' + ? await (uln as any).get_raw_oapp_uln_send_config(_address, _eid) + : await (uln as any).get_raw_oapp_uln_receive_config(_address, _eid) + return { + confirmations: config.has_confirmations ? BigInt(config.confirmations) : 0n, + requiredDVNs: config.has_required_dvns ? this.normalizeAddressArray(config.required_dvns) : [], + requiredDVNCount: config.has_required_dvns ? config.required_dvns.length : 0, + optionalDVNs: config.has_optional_dvns ? this.normalizeAddressArray(config.optional_dvns) : [], + optionalDVNThreshold: config.has_optional_dvns ? Number(config.optional_dvn_threshold) : 0, + } + } + + async hasAppUlnConfig( + _eid: EndpointId, + _oapp: OmniAddress, + _config: Uln302UlnUserConfig, + _type: Uln302ConfigType + ): Promise { + const current = await this.getAppUlnConfig(_eid, _oapp, _type) + + const confirmationsMatch = current.confirmations === (_config.confirmations ?? current.confirmations) + const requiredDvnsMatch = this.equalAddressArrays(current.requiredDVNs, _config.requiredDVNs) + const optionalDvnsMatch = this.equalAddressArrays(current.optionalDVNs, _config.optionalDVNs ?? []) + const thresholdMatch = current.optionalDVNThreshold === (_config.optionalDVNThreshold ?? 0) + + return confirmationsMatch && requiredDvnsMatch && optionalDvnsMatch && thresholdMatch + } + + async setDefaultUlnConfig(_eid: EndpointId, _config: Uln302UlnUserConfig): Promise { + return this.notImplemented('setDefaultUlnConfig') + } + + async getExecutorConfig( + _eid: EndpointId, + _address?: OmniAddress | null | undefined + ): Promise { + if (!_address) { + return { maxMessageSize: 0, executor: '0x0' } + } + return this.getAppExecutorConfig(_eid, _address) + } + + async getAppExecutorConfig(_eid: EndpointId, _address: OmniAddress): Promise { + const uln = await this.getUln() + const config = await (uln as any).get_raw_oapp_executor_config(_address, _eid) + return { + maxMessageSize: Number(config.max_message_size ?? 0), + executor: this.normalizeAddress(config.executor) ?? '0x0', + } + } + + async hasAppExecutorConfig(_eid: EndpointId, _oapp: OmniAddress, _config: Uln302ExecutorConfig): Promise { + const current = await this.getAppExecutorConfig(_eid, _oapp) + return current.maxMessageSize === _config.maxMessageSize && areBytes32Equal(current.executor, _config.executor) + } + + async setDefaultExecutorConfig(_eid: EndpointId, _config: Uln302ExecutorConfig): Promise { + return this.notImplemented('setDefaultExecutorConfig') + } + + private notImplemented(method: string): never { + throw new TypeError(`${method}() not implemented on Starknet ULN302 SDK`) + } + + private async getUln(): Promise { + if (!this.uln) { + this.uln = await getUltraLightNodeContractWithAddress(this.point.address, this.provider) + } + return this.uln! + } + + private emptyUlnConfig(): Uln302UlnConfig { + return { + confirmations: 0n, + requiredDVNs: [], + requiredDVNCount: 0, + optionalDVNs: [], + optionalDVNThreshold: 0, + } + } + + private equalAddressArrays(left: string[], right: string[]): boolean { + if (left.length !== right.length) { + return false + } + // Use areBytes32Equal for address comparison to handle leading zero differences + const normalizedLeft = [...left].sort() + const normalizedRight = [...right].sort() + return normalizedLeft.every((value, index) => areBytes32Equal(value, normalizedRight[index])) + } + + private normalizeAddress(value: unknown): string | undefined { + if (value == null) { + return undefined + } + let hexValue: string | undefined + if (typeof value === 'string') { + hexValue = value + } else if (typeof value === 'bigint') { + hexValue = `0x${value.toString(16)}` + } + // Normalize Starknet felt252 addresses by removing leading zeros after 0x + if (hexValue) { + const normalized = hexValue.toLowerCase().replace(/^0x0*/, '0x') + return normalized === '0x' ? '0x0' : normalized + } + return undefined + } + + private normalizeAddressArray(addresses: unknown[]): string[] { + return addresses.map((addr) => this.normalizeAddress(addr) ?? '0x0') + } +} diff --git a/packages/protocol-devtools-starknet/tsconfig.json b/packages/protocol-devtools-starknet/tsconfig.json new file mode 100644 index 0000000000..12774c873d --- /dev/null +++ b/packages/protocol-devtools-starknet/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["dist", "node_modules"], + "include": ["src", "test", "*.config.ts"], + "compilerOptions": { + "target": "es2020", + "experimentalDecorators": true, + "types": ["node", "jest"], + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/protocol-devtools-starknet/tsup.config.ts b/packages/protocol-devtools-starknet/tsup.config.ts new file mode 100644 index 0000000000..7ef46a5ad1 --- /dev/null +++ b/packages/protocol-devtools-starknet/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + entry: ['src/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, +]) diff --git a/packages/ua-devtools-starknet/.eslintignore b/packages/ua-devtools-starknet/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/packages/ua-devtools-starknet/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/ua-devtools-starknet/.eslintrc.json b/packages/ua-devtools-starknet/.eslintrc.json new file mode 100644 index 0000000000..be97c53fbb --- /dev/null +++ b/packages/ua-devtools-starknet/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/ua-devtools-starknet/.prettierignore b/packages/ua-devtools-starknet/.prettierignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/packages/ua-devtools-starknet/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/packages/ua-devtools-starknet/CHANGELOG.md b/packages/ua-devtools-starknet/CHANGELOG.md new file mode 100644 index 0000000000..cbb231d0ce --- /dev/null +++ b/packages/ua-devtools-starknet/CHANGELOG.md @@ -0,0 +1,5 @@ +# @layerzerolabs/ua-devtools-starknet + +## 0.1.0 + +- Initial release. diff --git a/packages/ua-devtools-starknet/README.md b/packages/ua-devtools-starknet/README.md new file mode 100644 index 0000000000..ccaec12655 --- /dev/null +++ b/packages/ua-devtools-starknet/README.md @@ -0,0 +1,19 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/ua-devtools-starknet

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +Utilities for working with LayerZero Starknet contracts. diff --git a/packages/ua-devtools-starknet/bin/test b/packages/ua-devtools-starknet/bin/test new file mode 100755 index 0000000000..cb0085aeda --- /dev/null +++ b/packages/ua-devtools-starknet/bin/test @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ -z "${LZ_DEVTOOLS_ENABLE_STARKNET_TESTS}" ]; then + echo 'Starknet tests can be enabled by setting LZ_DEVTOOLS_ENABLE_STARKNET_TESTS environment variable to a non-empty value' +else + jest --ci "$@" +fi diff --git a/packages/ua-devtools-starknet/jest.config.js b/packages/ua-devtools-starknet/jest.config.js new file mode 100644 index 0000000000..04c8477342 --- /dev/null +++ b/packages/ua-devtools-starknet/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + cache: false, + reporters: [['github-actions', { silent: false }], 'default'], + testEnvironment: 'node', + testTimeout: 60_000, + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, +}; diff --git a/packages/ua-devtools-starknet/jest.setup.js b/packages/ua-devtools-starknet/jest.setup.js new file mode 100644 index 0000000000..956faa4d5c --- /dev/null +++ b/packages/ua-devtools-starknet/jest.setup.js @@ -0,0 +1,4 @@ +import * as jestExtended from 'jest-extended'; + +// add all jest-extended matchers +expect.extend(jestExtended); diff --git a/packages/ua-devtools-starknet/package.json b/packages/ua-devtools-starknet/package.json new file mode 100644 index 0000000000..8cfa2d3fe9 --- /dev/null +++ b/packages/ua-devtools-starknet/package.json @@ -0,0 +1,83 @@ +{ + "name": "@layerzerolabs/ua-devtools-starknet", + "version": "0.1.0", + "description": "Utilities for LayerZero Starknet projects", + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/devtools.git", + "directory": "packages/ua-devtools-starknet" + }, + "license": "MIT", + "sideEffects": false, + "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": "$npm_execpath tsc --noEmit", + "build": "$npm_execpath tsup", + "clean": "rm -rf dist", + "dev": "$npm_execpath tsup --watch", + "lint": "$npm_execpath eslint '**/*.{js,ts,json}'", + "lint:fix": "eslint --fix '**/*.{js,ts,json}'", + "test": "./bin/test" + }, + "dependencies": { + "p-memoize": "~4.0.4" + }, + "devDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-starknet": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/oft-mint-burn-starknet": "^0.2.19", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@layerzerolabs/protocol-devtools-starknet": "~0.1.0", + "@layerzerolabs/protocol-starknet-v2": "^0.2.19", + "@layerzerolabs/test-devtools": "~0.4.7", + "@layerzerolabs/test-devtools-starknet": "~0.0.1", + "@layerzerolabs/ua-devtools": "~5.0.2", + "@swc/core": "^1.4.0", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.12", + "fast-check": "^3.15.1", + "jest": "^29.7.0", + "jest-extended": "^4.0.2", + "starknet": "^8.9.0", + "ts-node": "^10.9.2", + "tslib": "~2.6.2", + "tsup": "~8.0.1", + "typescript": "^5.4.4" + }, + "peerDependencies": { + "@layerzerolabs/devtools": "~2.0.4", + "@layerzerolabs/devtools-starknet": "~0.1.0", + "@layerzerolabs/io-devtools": "~0.3.2", + "@layerzerolabs/lz-definitions": "^3.0.148", + "@layerzerolabs/lz-v2-utilities": "^3.0.148", + "@layerzerolabs/oft-mint-burn-starknet": "^0.2.19", + "@layerzerolabs/protocol-devtools": "~3.0.2", + "@layerzerolabs/protocol-devtools-starknet": "~0.1.0", + "@layerzerolabs/protocol-starknet-v2": "^0.2.19", + "@layerzerolabs/ua-devtools": "~5.0.2", + "starknet": "^8.9.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/ua-devtools-starknet/src/index.ts b/packages/ua-devtools-starknet/src/index.ts new file mode 100644 index 0000000000..ab18876664 --- /dev/null +++ b/packages/ua-devtools-starknet/src/index.ts @@ -0,0 +1 @@ +export * from './oft' diff --git a/packages/ua-devtools-starknet/src/oft/config.ts b/packages/ua-devtools-starknet/src/oft/config.ts new file mode 100644 index 0000000000..a24f7146fe --- /dev/null +++ b/packages/ua-devtools-starknet/src/oft/config.ts @@ -0,0 +1,44 @@ +import { + type OmniVector, + type CreateTransactionsFromOmniEdges, + formatOmniVector, + createConfigureEdges, + createConfigureMultiple, + OmniSDKFactory, + OmniPoint, +} from '@layerzerolabs/devtools' +import { createModuleLogger, createWithAsyncLogger } from '@layerzerolabs/io-devtools' +import { isOmniPointOnStarknet } from '@layerzerolabs/devtools-starknet' +import type { IOApp, OAppConfigurator, OAppOmniGraph } from '@layerzerolabs/ua-devtools' +import { OFT } from './sdk' + +const createOFTLogger = () => createModuleLogger('OFT') +const withOFTLogger = createWithAsyncLogger(createOFTLogger) + +const isVectorFromStarknet = (vector: OmniVector): boolean => isOmniPointOnStarknet(vector.from) + +const onlyEdgesFromStarknet = ( + createTransactions: CreateTransactionsFromOmniEdges +): CreateTransactionsFromOmniEdges => { + const logger = createOFTLogger() + + return (edge, sdk, graph, createSdk) => { + if (!isVectorFromStarknet(edge.vector)) { + return logger.verbose(`Ignoring connection ${formatOmniVector(edge.vector)}`), undefined + } + + return createTransactions(edge, sdk as OFT, graph, createSdk as OmniSDKFactory) + } +} + +export const initConfig: OAppConfigurator = createConfigureEdges( + onlyEdgesFromStarknet( + withOFTLogger(async () => { + const logger = createOFTLogger() + logger.warn('Starknet OFT initConfig is not implemented yet') + return undefined + }) + ) +) + +export const initOFTAccounts = createConfigureMultiple(initConfig) diff --git a/packages/ua-devtools-starknet/src/oft/factory.ts b/packages/ua-devtools-starknet/src/oft/factory.ts new file mode 100644 index 0000000000..373e2340ce --- /dev/null +++ b/packages/ua-devtools-starknet/src/oft/factory.ts @@ -0,0 +1,15 @@ +import pMemoize from 'p-memoize' +import type { OAppFactory } from '@layerzerolabs/ua-devtools' +import { OFT } from './sdk' +import { type ConnectionFactory, createConnectionFactory, defaultRpcUrlFactory } from '@layerzerolabs/devtools-starknet' + +/** + * Syntactic sugar that creates an instance of Starknet `OFT` SDK + * based on an `OmniPoint` with help of an `ConnectionFactory`. + * + * @param {ConnectionFactory} connectionFactory A function that returns a `RpcProvider` based on an `EndpointId` + * @returns {OAppFactory} + */ +export const createOFTFactory = ( + connectionFactory: ConnectionFactory = createConnectionFactory(defaultRpcUrlFactory) +): OAppFactory => pMemoize(async (point) => new OFT(await connectionFactory(point.eid), point)) diff --git a/packages/ua-devtools-starknet/src/oft/index.ts b/packages/ua-devtools-starknet/src/oft/index.ts new file mode 100644 index 0000000000..444d96c1a3 --- /dev/null +++ b/packages/ua-devtools-starknet/src/oft/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './factory' +export * from './sdk' diff --git a/packages/ua-devtools-starknet/src/oft/sdk.ts b/packages/ua-devtools-starknet/src/oft/sdk.ts new file mode 100644 index 0000000000..df697ad74b --- /dev/null +++ b/packages/ua-devtools-starknet/src/oft/sdk.ts @@ -0,0 +1,254 @@ +import type { IOApp, OAppEnforcedOptionParam } from '@layerzerolabs/ua-devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import { formatEid, areBytes32Equal, type Bytes, type OmniAddress, type OmniTransaction } from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-starknet' +import { EndpointV2, STARKNET_ENDPOINT_V2_ADDRESSES } from '@layerzerolabs/protocol-devtools-starknet' +import { Contract } from 'starknet' + +// OFT ABI from oft-mint-burn-starknet package - includes enforced options functions +const getOftAbi = (): unknown[] => { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const pkg = require('@layerzerolabs/oft-mint-burn-starknet') + return pkg.abi?.oFTMintBurnAdapter + } catch { + // Fallback to generic OApp ABI if OFT package not available + // eslint-disable-next-line @typescript-eslint/no-var-requires + const protocol = require('@layerzerolabs/protocol-starknet-v2') + return protocol.abi?.oApp + } +} + +const getOFTContract = (address: string, provider: unknown): Contract => { + const abi = getOftAbi() + return new (Contract as any)({ abi, address, providerOrAccount: provider }) +} + +const normalizeHex = (value: string): string => (value.startsWith('0x') ? value.slice(2) : value) + +const toFixedBytes = (value: unknown, size: number): Buffer => { + if (typeof value === 'string') { + const hex = normalizeHex(value).padStart(size * 2, '0') + return Buffer.from(hex.slice(-size * 2), 'hex') + } + if (typeof value === 'bigint') { + const hex = value.toString(16).padStart(size * 2, '0') + return Buffer.from(hex.slice(-size * 2), 'hex') + } + if (typeof value === 'number') { + return toFixedBytes(BigInt(value), size) + } + return Buffer.alloc(size) +} + +/** + * Convert hex string to a string for Cairo ByteArray. + * In starknet.js v8, ByteArray parameters accept plain strings. + * We convert hex bytes to latin1 encoding which preserves byte values. + */ +const toCairoByteArray = (hex: string): string => { + const clean = normalizeHex(hex || '') + if (!clean) { + return '' + } + const buffer = Buffer.from(clean, 'hex') + return buffer.toString('latin1') +} + +const fromCairoByteArray = (value: unknown): string => { + if (value == null) { + return '0x' + } + if (typeof value === 'string') { + return value.startsWith('0x') ? value : `0x${value}` + } + if (value instanceof Uint8Array || Buffer.isBuffer(value)) { + return `0x${Buffer.from(value).toString('hex')}` + } + if (typeof value === 'object' && value !== null && 'data' in value && 'pending_word' in value) { + const data = (value as { data?: unknown[] }).data ?? [] + const pendingWord = (value as { pending_word?: unknown }).pending_word + const pendingLen = Number((value as { pending_word_len?: unknown }).pending_word_len ?? 0) + const chunks = data.map((entry) => toFixedBytes(entry, 31)) + const pending = pendingLen ? toFixedBytes(pendingWord, pendingLen) : Buffer.alloc(0) + return `0x${Buffer.concat([...chunks, pending]).toString('hex')}` + } + return '0x' +} + +export class OFT extends OmniSDK implements IOApp { + private oapp?: Contract + + async getEndpointSDK(): Promise { + const endpoint = STARKNET_ENDPOINT_V2_ADDRESSES[this.point.eid] + if (!endpoint) { + throw new Error( + `No Starknet EndpointV2 address configured for eid ${this.point.eid} (${formatEid(this.point.eid)})` + ) + } + return new EndpointV2(this.provider, { eid: this.point.eid, address: endpoint }) + } + + async getOwner(): Promise { + return this.getDelegate() + } + + async hasOwner(_address: OmniAddress): Promise { + const owner = await this.getOwner() + return owner === _address + } + + async setOwner(_address: OmniAddress): Promise { + return this.setDelegate(_address) + } + + async getPeer(_eid: EndpointId): Promise { + const oapp = await this.getOApp() + if (!('get_peer' in oapp)) { + return this.notImplemented('getPeer') + } + const result = await (oapp as any).get_peer(_eid) + return this.parseFelt(result?.value ?? result) + } + + async hasPeer(_eid: EndpointId, _address: OmniAddress | null | undefined): Promise { + const peer = await this.getPeer(_eid) + // Both null/undefined = no peer set + if (peer == null && _address == null) { + return true + } + // One is null, other is not + if (peer == null || _address == null) { + return false + } + // Use areBytes32Equal for address comparison to handle leading zero differences + return areBytes32Equal(peer, _address) + } + + async setPeer(_eid: EndpointId, _peer: OmniAddress | null | undefined): Promise { + const oapp = await this.getOApp() + const peerValue = _peer ? { value: BigInt(_peer) } : { value: 0n } + const call = (oapp as any).populateTransaction.set_peer(_eid, peerValue) + return { + ...this.createTransaction([call]), + description: `Setting peer for ${formatEid(_eid)} to ${_peer ?? '0x0'}`, + } + } + + async getDelegate(): Promise { + const oapp = await this.getOApp() + if (!('get_delegate' in oapp)) { + return this.notImplemented('getDelegate') + } + const result = await (oapp as any).get_delegate() + return this.parseFelt(result) + } + + async isDelegate(_address: OmniAddress): Promise { + const delegate = await this.getDelegate() + return delegate === _address + } + + async setDelegate(_address: OmniAddress): Promise { + const oapp = await this.getOApp() + const call = (oapp as any).populateTransaction.set_delegate(_address) + return { + ...this.createTransaction([call]), + description: `Setting delegate to ${_address}`, + } + } + + async getEnforcedOptions(_eid: EndpointId, _msgType: number): Promise { + try { + const oapp = await this.getOApp() + if (!('get_enforced_options' in oapp)) { + // Contract doesn't expose this function - return empty options + return '0x' + } + const result = await (oapp as any).get_enforced_options(_eid, _msgType) + return fromCairoByteArray(result) + } catch (error) { + // If the call fails (e.g., no enforced options set), return empty options + if (this.isMissingStarknetConfig(error)) { + return '0x' + } + throw error + } + } + + private isMissingStarknetConfig(error: unknown): boolean { + const message = + typeof error === 'string' + ? error.toLowerCase() + : error && typeof error === 'object' && 'message' in error + ? String((error as { message?: unknown }).message).toLowerCase() + : '' + if (!message) { + return false + } + // Common patterns for missing config in Starknet + return message.includes('entry point') || message.includes('not found') || message.includes('contract error') + } + + async setEnforcedOptions(_enforcedOptions: OAppEnforcedOptionParam[]): Promise { + const oapp = await this.getOApp() + // Try both function names - the contract may use either form + const funcName = + 'set_enforced_options' in oapp + ? 'set_enforced_options' + : 'setEnforcedOptions' in oapp + ? 'setEnforcedOptions' + : null + if (!funcName) { + // Contract doesn't support enforced options - return a no-op transaction + this.logger.warn(`Contract at ${this.point.address} does not support setEnforcedOptions - skipping`) + return this.createTransaction([]) + } + const params = _enforcedOptions.map(({ eid, option }) => ({ + eid, + msg_type: option.msgType, + options: toCairoByteArray(option.options), + })) + const call = (oapp as any).populateTransaction[funcName](params) + return { + ...this.createTransaction([call]), + description: `Setting enforced options for ${params.length} pathway(s)`, + } + } + + async getCallerBpsCap(): Promise { + return this.notImplemented('getCallerBpsCap') + } + + async setCallerBpsCap(_callerBpsCap: bigint): Promise { + return this.notImplemented('setCallerBpsCap') + } + + private notImplemented(method: string): never { + throw new TypeError(`${method}() not implemented on Starknet OFT SDK`) + } + + private async getOApp(): Promise { + if (!this.oapp) { + this.oapp = getOFTContract(this.point.address, this.provider) + } + return this.oapp! + } + + private parseFelt(value: unknown): string | undefined { + if (value == null) { + return undefined + } + if (typeof value === 'string') { + return value + } + if (typeof value === 'bigint') { + return `0x${value.toString(16)}` + } + if (typeof value === 'object' && value !== null && 'value' in value) { + const feltValue = (value as { value: bigint | string }).value + return typeof feltValue === 'bigint' ? `0x${feltValue.toString(16)}` : String(feltValue) + } + return undefined + } +} diff --git a/packages/ua-devtools-starknet/tsconfig.json b/packages/ua-devtools-starknet/tsconfig.json new file mode 100644 index 0000000000..12774c873d --- /dev/null +++ b/packages/ua-devtools-starknet/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["dist", "node_modules"], + "include": ["src", "test", "*.config.ts"], + "compilerOptions": { + "target": "es2020", + "experimentalDecorators": true, + "types": ["node", "jest"], + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/ua-devtools-starknet/tsup.config.ts b/packages/ua-devtools-starknet/tsup.config.ts new file mode 100644 index 0000000000..7ef46a5ad1 --- /dev/null +++ b/packages/ua-devtools-starknet/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + entry: ['src/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, +]) diff --git a/packages/ua-devtools/src/oapp/config.ts b/packages/ua-devtools/src/oapp/config.ts index 43c1c8b5e2..8bc8366a34 100644 --- a/packages/ua-devtools/src/oapp/config.ts +++ b/packages/ua-devtools/src/oapp/config.ts @@ -9,6 +9,7 @@ import { createConfigureMultiple, createConfigureNodes, createConfigureEdges, + areBytes32Equal, } from '@layerzerolabs/devtools' import type { OAppConfigurator, OAppEnforcedOption, OAppEnforcedOptionParam, OAppFactory } from './types' import { createModuleLogger, createWithAsyncLogger, printBoolean } from '@layerzerolabs/io-devtools' @@ -137,9 +138,13 @@ export const configureSendLibraries: OAppConfigurator = withOAppLogger( ) } - if (!isDefaultLibrary && currentSendLibrary?.toLowerCase() === config.sendLibrary.toLowerCase()) { + // Skip if the current library already matches the configured library + // This handles both cases: + // 1. Non-default library that matches config + // 2. Default library that matches config (avoids SAME_VALUE errors) + if (areBytes32Equal(currentSendLibrary, config.sendLibrary)) { logger.verbose( - `Current sendLibrary is not default library and is already set to ${config.sendLibrary} for ${formatOmniVector({ from, to })}, skipping` + `Current sendLibrary is already set to ${config.sendLibrary} for ${formatOmniVector({ from, to })}${isDefaultLibrary ? ' (default)' : ''}, skipping` ) return [] } @@ -212,9 +217,13 @@ export const configureReceiveLibraries: OAppConfigurator = withOAppLogger( ) } - if (!isDefaultLibrary && currentReceiveLibrary === config.receiveLibraryConfig.receiveLibrary) { + // Skip if the current library already matches the configured library + // This handles both cases: + // 1. Non-default library that matches config + // 2. Default library that matches config (avoids SAME_VALUE errors) + if (areBytes32Equal(currentReceiveLibrary, config.receiveLibraryConfig.receiveLibrary)) { logger.verbose( - `Current recieveLibrary is not default and is already set to ${config.receiveLibraryConfig.receiveLibrary} for ${formatOmniVector({ from, to })}, skipping` + `Current recieveLibrary is already set to ${config.receiveLibraryConfig.receiveLibrary} for ${formatOmniVector({ from, to })}${isDefaultLibrary ? ' (default)' : ''}, skipping` ) return [] } From 750457b0055ba09ca614aaf82fc6dcf3216b989c Mon Sep 17 00:00:00 2001 From: Krak Date: Mon, 26 Jan 2026 14:47:41 -0800 Subject: [PATCH 5/8] feat(devtools-starknet): fix ByteArray encoding and add wire support Key fixes: - Fix fromHex to handle odd-length hex strings by padding with leading '0' (Buffer.from silently truncates odd-length strings) - Fix ByteArray encoding in setEnforcedOptions to use raw calldata instead of string-based encoding (starknet.js UTF-8 re-encodes bytes >= 128) - Update sendStarknet to use createRpcUrlFactory() for RPC URL resolution - Update starknet.js v8 Account constructor format These fixes resolve the "out of bound" error when sending from Starknet OFT caused by corrupted enforced options (byte 0x80 becoming UTF-8 0xc2 0x80). --- packages/devtools/src/common/bytes.ts | 10 ++- packages/ua-devtools-starknet/src/oft/sdk.ts | 84 +++++++++++++++++--- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/packages/devtools/src/common/bytes.ts b/packages/devtools/src/common/bytes.ts index df9f5004b6..28646f22c7 100644 --- a/packages/devtools/src/common/bytes.ts +++ b/packages/devtools/src/common/bytes.ts @@ -196,10 +196,18 @@ export const toHex = (bytes: Uint8Array): string => `0x${Buffer.from(bytes).toSt /** * Helper utility to convert a hex string (with or without leading `0x`) to `UInt8Array` * + * Note: Buffer.from(str, 'hex') silently truncates the last character if the string + * has odd length. We pad with a leading '0' to prevent data loss. + * * @param {string} hex * @returns {Uint8Array} */ -export const fromHex = (hex: string): Uint8Array => Uint8Array.from(Buffer.from(hex.replace(/^0x/, ''), 'hex')) +export const fromHex = (hex: string): Uint8Array => { + const stripped = hex.replace(/^0x/, '') + // Pad with leading '0' if odd length to prevent Buffer.from truncation + const padded = stripped.length % 2 === 1 ? '0' + stripped : stripped + return Uint8Array.from(Buffer.from(padded, 'hex')) +} /** * Helper utility that returns the leftmost bytes after removing the rightmost `length` bytes from a UInt8Array. diff --git a/packages/ua-devtools-starknet/src/oft/sdk.ts b/packages/ua-devtools-starknet/src/oft/sdk.ts index df697ad74b..5b5cd1f32f 100644 --- a/packages/ua-devtools-starknet/src/oft/sdk.ts +++ b/packages/ua-devtools-starknet/src/oft/sdk.ts @@ -41,12 +41,59 @@ const toFixedBytes = (value: unknown, size: number): Buffer => { return Buffer.alloc(size) } +/** + * Convert hex string to flat calldata for Cairo ByteArray. + * + * IMPORTANT: We cannot use starknet.js's string-based ByteArray encoding + * because it re-encodes strings as UTF-8, corrupting bytes >= 128. + * For example, byte 0x80 becomes 0xc2 0x80 in UTF-8. + * + * Instead, we return the flat calldata representation that matches Cairo's + * ByteArray serialization format: + * [data_len, ...data_words, pending_word, pending_word_len] + * + * This is used with Calldata.compile() to bypass starknet.js's ByteArray handling. + */ +const hexToByteArrayCalldata = (hex: string): string[] => { + const clean = normalizeHex(hex || '') + if (!clean) { + return ['0', '0x0', '0'] + } + + const buffer = Buffer.from(clean, 'hex') + const calldata: string[] = [] + + // Each data chunk is 31 bytes + const chunkSize = 31 + let offset = 0 + const dataWords: string[] = [] + + // Process full 31-byte chunks + while (offset + chunkSize <= buffer.length) { + const chunk = buffer.subarray(offset, offset + chunkSize) + dataWords.push('0x' + chunk.toString('hex')) + offset += chunkSize + } + + // Add data array: length followed by elements + calldata.push(dataWords.length.toString()) + calldata.push(...dataWords) + + // Remaining bytes go into pending_word + const remaining = buffer.subarray(offset) + const pendingWord = remaining.length > 0 ? '0x' + remaining.toString('hex') : '0x0' + calldata.push(pendingWord) + calldata.push(remaining.length.toString()) + + return calldata +} + /** * Convert hex string to a string for Cairo ByteArray. - * In starknet.js v8, ByteArray parameters accept plain strings. - * We convert hex bytes to latin1 encoding which preserves byte values. + * This is kept for backward compatibility but should only be used + * for data that doesn't contain bytes >= 128. */ -const toCairoByteArray = (hex: string): string => { +const _toCairoByteArray = (hex: string): string => { const clean = normalizeHex(hex || '') if (!clean) { return '' @@ -204,15 +251,32 @@ export class OFT extends OmniSDK implements IOApp { this.logger.warn(`Contract at ${this.point.address} does not support setEnforcedOptions - skipping`) return this.createTransaction([]) } - const params = _enforcedOptions.map(({ eid, option }) => ({ - eid, - msg_type: option.msgType, - options: toCairoByteArray(option.options), - })) - const call = (oapp as any).populateTransaction[funcName](params) + + // Build calldata manually to avoid UTF-8 corruption of ByteArray data + // The function signature is: set_enforced_options(params: Array<{eid: u32, msg_type: u8, options: ByteArray}>) + // Calldata format: [array_len, param1_eid, param1_msg_type, param1_options..., param2_eid, ...] + const calldata: string[] = [] + + // Array length + calldata.push(_enforcedOptions.length.toString()) + + // Each param: eid (u32), msg_type (u8), options (ByteArray) + for (const { eid, option } of _enforcedOptions) { + calldata.push(eid.toString()) + calldata.push(option.msgType.toString()) + // ByteArray is serialized as: [data_len, ...data_words, pending_word, pending_word_len] + calldata.push(...hexToByteArrayCalldata(option.options)) + } + + const call = { + contractAddress: this.point.address, + entrypoint: funcName, + calldata, + } + return { ...this.createTransaction([call]), - description: `Setting enforced options for ${params.length} pathway(s)`, + description: `Setting enforced options for ${_enforcedOptions.length} pathway(s)`, } } From 6388eb7797f8cf25d1013c993e415485ea79d632 Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 27 Jan 2026 11:15:37 -0800 Subject: [PATCH 6/8] fix: update lockfile for new Sui and Starknet packages --- pnpm-lock.yaml | 695 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 644 insertions(+), 51 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52972fbc99..45d4571e27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4183,6 +4183,107 @@ importers: specifier: ^3.22.4 version: 3.22.4 + packages/devtools-starknet: + dependencies: + p-memoize: + specifier: ~4.0.4 + version: 4.0.4 + devDependencies: + '@layerzerolabs/devtools': + specifier: ~2.0.4 + version: link:../devtools + '@layerzerolabs/io-devtools': + specifier: ~0.3.2 + version: link:../io-devtools + '@layerzerolabs/lz-definitions': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/protocol-starknet-v2': + specifier: ^0.2.19 + version: 0.2.20 + '@swc/core': + specifier: ^1.4.0 + version: 1.4.0 + '@swc/jest': + specifier: ^0.2.36 + version: 0.2.36(@swc/core@1.4.0) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + starknet: + specifier: ^8.9.0 + version: 8.9.2 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + + packages/devtools-sui: + dependencies: + p-memoize: + specifier: ~4.0.4 + version: 4.0.4 + devDependencies: + '@layerzerolabs/devtools': + specifier: ~2.0.4 + version: link:../devtools + '@layerzerolabs/io-devtools': + specifier: ~0.3.2 + version: link:../io-devtools + '@layerzerolabs/lz-definitions': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/lz-sui-oft-sdk-v2': + specifier: ^3.0.156 + version: 3.0.156(typescript@5.5.3) + '@layerzerolabs/lz-sui-sdk-v2': + specifier: ^3.0.156 + version: 3.0.156(typescript@5.5.3) + '@mysten/bcs': + specifier: ^1.9.2 + version: 1.9.2 + '@mysten/sui': + specifier: ^1.45.2 + version: 1.45.2(typescript@5.5.3) + '@swc/core': + specifier: ^1.4.0 + version: 1.4.0 + '@swc/jest': + specifier: ^0.2.36 + version: 0.2.36(@swc/core@1.4.0) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + packages/devtools-ton: dependencies: '@ton/core': @@ -4273,7 +4374,7 @@ importers: version: 2.16.2 jest: specifier: ^29.6.2 - version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + version: 29.7.0(@types/node@18.18.14) tsup: specifier: ~8.0.1 version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3) @@ -4470,7 +4571,7 @@ importers: version: 29.5.12 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + version: 29.7.0(@types/node@18.18.14) tslib: specifier: ~2.6.2 version: 2.6.3 @@ -5064,13 +5165,13 @@ importers: version: 3.0.148 '@layerzerolabs/lz-solana-sdk-v2': specifier: ^3.0.118 - version: 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.148 version: 3.0.148 '@layerzerolabs/oft-v2-solana-sdk': specifier: ^3.0.38 - version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/protocol-devtools': specifier: ~3.0.2 version: link:../protocol-devtools @@ -5129,6 +5230,146 @@ importers: specifier: ^3.22.4 version: 3.22.4 + packages/protocol-devtools-starknet: + dependencies: + p-memoize: + specifier: ~4.0.4 + version: 4.0.4 + devDependencies: + '@layerzerolabs/devtools': + specifier: ~2.0.4 + version: link:../devtools + '@layerzerolabs/devtools-starknet': + specifier: ~0.1.0 + version: link:../devtools-starknet + '@layerzerolabs/io-devtools': + specifier: ~0.3.2 + version: link:../io-devtools + '@layerzerolabs/lz-definitions': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/protocol-devtools': + specifier: ~3.0.2 + version: link:../protocol-devtools + '@layerzerolabs/protocol-starknet-v2': + specifier: ^0.2.19 + version: 0.2.20 + '@layerzerolabs/test-devtools': + specifier: ~0.4.7 + version: link:../test-devtools + '@layerzerolabs/test-devtools-starknet': + specifier: ~0.0.1 + version: link:../test-devtools-starknet + '@swc/core': + specifier: ^1.4.0 + version: 1.4.0 + '@swc/jest': + specifier: ^0.2.36 + version: 0.2.36(@swc/core@1.4.0) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + fast-check: + specifier: ^3.15.1 + version: 3.23.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + jest-extended: + specifier: ^4.0.2 + version: 4.0.2(jest@29.7.0) + starknet: + specifier: ^8.9.0 + version: 8.9.2 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + zod: + specifier: ^3.22.4 + version: 3.22.4 + + packages/protocol-devtools-sui: + dependencies: + p-memoize: + specifier: ~4.0.4 + version: 4.0.4 + devDependencies: + '@layerzerolabs/devtools': + specifier: ~2.0.4 + version: link:../devtools + '@layerzerolabs/devtools-sui': + specifier: ~0.1.0 + version: link:../devtools-sui + '@layerzerolabs/io-devtools': + specifier: ~0.3.2 + version: link:../io-devtools + '@layerzerolabs/lz-definitions': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/lz-sui-sdk-v2': + specifier: ^3.0.156 + version: 3.0.156(typescript@5.5.3) + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/protocol-devtools': + specifier: ~3.0.2 + version: link:../protocol-devtools + '@layerzerolabs/test-devtools': + specifier: ~0.4.7 + version: link:../test-devtools + '@layerzerolabs/test-devtools-sui': + specifier: ~0.0.1 + version: link:../test-devtools-sui + '@mysten/sui': + specifier: ^1.45.2 + version: 1.45.2(typescript@5.5.3) + '@swc/core': + specifier: ^1.4.0 + version: 1.4.0 + '@swc/jest': + specifier: ^0.2.36 + version: 0.2.36(@swc/core@1.4.0) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + fast-check: + specifier: ^3.15.1 + version: 3.23.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + jest-extended: + specifier: ^4.0.2 + version: 4.0.2(jest@29.7.0) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + zod: + specifier: ^3.22.4 + version: 3.22.4 + packages/script-devtools-evm-foundry: dependencies: '@layerzerolabs/oapp-evm': @@ -5303,6 +5544,42 @@ importers: specifier: ^5.4.4 version: 5.5.3 + packages/test-devtools-starknet: + devDependencies: + fast-check: + specifier: ^3.15.1 + version: 3.23.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + + packages/test-devtools-sui: + devDependencies: + fast-check: + specifier: ^3.15.1 + version: 3.23.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + packages/test-devtools-ton: devDependencies: '@swc/core': @@ -5722,13 +5999,13 @@ importers: version: 3.0.148 '@layerzerolabs/lz-solana-sdk-v2': specifier: ^3.0.118 - version: 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.148 version: 3.0.148 '@layerzerolabs/oft-v2-solana-sdk': specifier: ^3.0.59 - version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/protocol-devtools': specifier: ~3.0.2 version: link:../protocol-devtools @@ -5796,6 +6073,158 @@ importers: specifier: ^3.22.4 version: 3.22.4 + packages/ua-devtools-starknet: + dependencies: + p-memoize: + specifier: ~4.0.4 + version: 4.0.4 + devDependencies: + '@layerzerolabs/devtools': + specifier: ~2.0.4 + version: link:../devtools + '@layerzerolabs/devtools-starknet': + specifier: ~0.1.0 + version: link:../devtools-starknet + '@layerzerolabs/io-devtools': + specifier: ~0.3.2 + version: link:../io-devtools + '@layerzerolabs/lz-definitions': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/oft-mint-burn-starknet': + specifier: ^0.2.19 + version: 0.2.20(@layerzerolabs/protocol-starknet-v2@0.2.20) + '@layerzerolabs/protocol-devtools': + specifier: ~3.0.2 + version: link:../protocol-devtools + '@layerzerolabs/protocol-devtools-starknet': + specifier: ~0.1.0 + version: link:../protocol-devtools-starknet + '@layerzerolabs/protocol-starknet-v2': + specifier: ^0.2.19 + version: 0.2.20 + '@layerzerolabs/test-devtools': + specifier: ~0.4.7 + version: link:../test-devtools + '@layerzerolabs/test-devtools-starknet': + specifier: ~0.0.1 + version: link:../test-devtools-starknet + '@layerzerolabs/ua-devtools': + specifier: ~5.0.2 + version: link:../ua-devtools + '@swc/core': + specifier: ^1.4.0 + version: 1.4.0 + '@swc/jest': + specifier: ^0.2.36 + version: 0.2.36(@swc/core@1.4.0) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + fast-check: + specifier: ^3.15.1 + version: 3.23.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + jest-extended: + specifier: ^4.0.2 + version: 4.0.2(jest@29.7.0) + starknet: + specifier: ^8.9.0 + version: 8.9.2 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + + packages/ua-devtools-sui: + dependencies: + p-memoize: + specifier: ~4.0.4 + version: 4.0.4 + devDependencies: + '@layerzerolabs/devtools': + specifier: ~2.0.4 + version: link:../devtools + '@layerzerolabs/devtools-sui': + specifier: ~0.1.0 + version: link:../devtools-sui + '@layerzerolabs/io-devtools': + specifier: ~0.3.2 + version: link:../io-devtools + '@layerzerolabs/lz-definitions': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/lz-sui-oft-sdk-v2': + specifier: ^3.0.156 + version: 3.0.156(typescript@5.5.3) + '@layerzerolabs/lz-sui-sdk-v2': + specifier: ^3.0.156 + version: 3.0.156(typescript@5.5.3) + '@layerzerolabs/lz-v2-utilities': + specifier: ^3.0.148 + version: 3.0.148 + '@layerzerolabs/protocol-devtools': + specifier: ~3.0.2 + version: link:../protocol-devtools + '@layerzerolabs/protocol-devtools-sui': + specifier: ~0.1.0 + version: link:../protocol-devtools-sui + '@layerzerolabs/test-devtools': + specifier: ~0.4.7 + version: link:../test-devtools + '@layerzerolabs/test-devtools-sui': + specifier: ~0.0.1 + version: link:../test-devtools-sui + '@layerzerolabs/ua-devtools': + specifier: ~5.0.2 + version: link:../ua-devtools + '@mysten/sui': + specifier: ^1.45.2 + version: 1.45.2(typescript@5.5.3) + '@swc/core': + specifier: ^1.4.0 + version: 1.4.0 + '@swc/jest': + specifier: ^0.2.36 + version: 0.2.36(@swc/core@1.4.0) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + fast-check: + specifier: ^3.15.1 + version: 3.23.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + jest-extended: + specifier: ^4.0.2 + version: 4.0.2(jest@29.7.0) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + tslib: + specifier: ~2.6.2 + version: 2.6.3 + tsup: + specifier: ~8.0.1 + version: 8.0.1(@swc/core@1.4.0)(ts-node@10.9.2)(typescript@5.5.3) + typescript: + specifier: ^5.4.4 + version: 5.5.3 + packages/verify-contract: devDependencies: '@ethersproject/abi': @@ -5833,7 +6262,7 @@ importers: version: 12.6.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + version: 29.7.0(@types/node@18.18.14) tsup: specifier: ^8.0.1 version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3) @@ -9121,7 +9550,7 @@ packages: '@ledgerhq/hw-transport': 6.31.4 '@ledgerhq/hw-transport-webhid': 6.30.0 '@ledgerhq/hw-transport-webusb': 6.29.4 - '@mysten/bcs': 1.2.0 + '@mysten/bcs': 1.9.2 axios: 1.8.4 bech32: 2.0.0 bignumber.js: 9.1.2 @@ -9149,7 +9578,7 @@ packages: '@ledgerhq/hw-transport': 6.31.4 '@ledgerhq/hw-transport-webhid': 6.30.0 '@ledgerhq/hw-transport-webusb': 6.29.4 - '@mysten/bcs': 1.2.0 + '@mysten/bcs': 1.9.2 axios: 1.8.4 bech32: 2.0.0 bignumber.js: 9.1.2 @@ -9177,7 +9606,7 @@ packages: '@ledgerhq/hw-transport': 6.31.4 '@ledgerhq/hw-transport-webhid': 6.30.0 '@ledgerhq/hw-transport-webusb': 6.29.4 - '@mysten/bcs': 1.2.0 + '@mysten/bcs': 1.9.2 axios: 1.8.4 bech32: 2.0.0 bignumber.js: 9.1.2 @@ -10491,7 +10920,7 @@ packages: '@metaplex-foundation/umi-program-repository': 0.9.2(@metaplex-foundation/umi@0.9.2) '@metaplex-foundation/umi-rpc-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.0) '@metaplex-foundation/umi-transaction-factory-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.0) - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 '@noble/secp256k1': 1.7.1 '@solana/web3.js': 1.98.0 bip39: 3.1.0 @@ -10648,6 +11077,12 @@ packages: dependencies: tiny-invariant: 1.3.3 + /@layerzerolabs/lz-definitions@3.0.156: + resolution: {integrity: sha512-9gXF+C3LJ3mwrrkVBvVwXaeRnZNFNFeEuA0ykp881IRMFmmcVKLYOkidFgmaItm70MishMrrt4Lo+wWuzkqqUg==} + dependencies: + tiny-invariant: 1.3.3 + dev: true + /@layerzerolabs/lz-definitions@3.0.75: resolution: {integrity: sha512-TIbFBCfuElg6WcND4HNUROTSAayBETDC0YrISVadByo3iM2baAi+rpGC1kdrOxOTRlSBetd2khTOUCd7/sZdOQ==} dependencies: @@ -11273,9 +11708,9 @@ packages: '@layerzerolabs/lz-definitions': 3.0.148 '@layerzerolabs/lz-utilities': 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@noble/ed25519': 1.7.3 - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 '@noble/secp256k1': 1.7.1 - '@scure/base': 1.2.4 + '@scure/base': 1.2.6 bech32: 2.0.0 memoizee: 0.4.17 transitivePeerDependencies: @@ -11404,9 +11839,9 @@ packages: '@layerzerolabs/lz-definitions': 3.0.148 '@layerzerolabs/lz-utilities': 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@noble/ed25519': 1.7.3 - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 '@noble/secp256k1': 1.7.1 - '@scure/base': 1.2.1 + '@scure/base': 1.2.6 transitivePeerDependencies: - '@jest/globals' - '@swc/core' @@ -11427,9 +11862,9 @@ packages: '@layerzerolabs/lz-definitions': 3.0.148 '@layerzerolabs/lz-utilities': 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@noble/ed25519': 1.7.3 - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 '@noble/secp256k1': 1.7.1 - '@scure/base': 1.2.1 + '@scure/base': 1.2.6 transitivePeerDependencies: - '@jest/globals' - '@swc/core' @@ -11611,7 +12046,7 @@ packages: resolution: {integrity: sha512-kk/0pPKuNzIXL1Fh8caH3daYA/Lv2OIPeD7z/VIBANdyCBiYgncF6QK8muoFJ1iIvoQSnpAmgwgww1DlnYa2NA==} dev: true - /@layerzerolabs/lz-solana-sdk-v2@3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-D09DF3kXtwBGwyI8WcLv8AWfUHrCTJwXoMe7U+EuXH+TOsOmwVhLAk8fulRjR9fHnvQvGEyl1blHUebtY2nt3A==} dependencies: '@layerzerolabs/lz-corekit-solana': 3.0.120(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) @@ -11724,7 +12159,7 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/lz-solana-sdk-v2@3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-e1PbfEQ/QJnHwL+wYxy/EjPsluf3y1NSZKenbwHNjF/I3BEjLRuLbE7V6rLO0HIG6rZMQXEkT/keo1+MTwd/gQ==} dependencies: '@layerzerolabs/lz-corekit-solana': 3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) @@ -11802,6 +12237,30 @@ packages: - utf-8-validate dev: false + /@layerzerolabs/lz-sui-oft-sdk-v2@3.0.156(typescript@5.5.3): + resolution: {integrity: sha512-zOvWGOD9HT1WdnY2nvVaG5Zw4Cqx3XeSQqxJBobt6TP1rLVlFN226BE8LVxEws8IdoomLD2Yz3BkR0BJS7l8bw==} + dependencies: + '@layerzerolabs/lz-definitions': 3.0.156 + '@layerzerolabs/lz-sui-sdk-v2': 3.0.156(typescript@5.5.3) + '@mysten/sui': 1.45.2(typescript@5.5.3) + transitivePeerDependencies: + - '@gql.tada/svelte-support' + - '@gql.tada/vue-support' + - typescript + dev: true + + /@layerzerolabs/lz-sui-sdk-v2@3.0.156(typescript@5.5.3): + resolution: {integrity: sha512-pQIl91brRk274hSiNS2NqUZ9U+l8zqPzeJ/1kOKPXZdiQIaz+/H2nwb0UXayxyAwXy3kl94ju/ecdOiKvHqAag==} + dependencies: + '@layerzerolabs/lz-definitions': 3.0.156 + '@mysten/sui': 1.45.2(typescript@5.5.3) + js-sha3: 0.8.0 + transitivePeerDependencies: + - '@gql.tada/svelte-support' + - '@gql.tada/vue-support' + - typescript + dev: true + /@layerzerolabs/lz-ton-sdk-v2@3.0.27: resolution: {integrity: sha512-AU1uOzmLjWvyHdJGTo689bXLsCS/QAmfQSjvZ4544muLfpGVLl3l6lOl8DwmN1UQuwKKK94C5rEvoElVUYf0zQ==} dependencies: @@ -11886,7 +12345,7 @@ packages: '@ethersproject/bytes': 5.8.0 '@initia/initia.js': 1.0.4(typescript@5.5.3) '@layerzerolabs/lz-definitions': 3.0.148 - '@mysten/sui': 1.39.0(typescript@5.5.3) + '@mysten/sui': 1.45.2(typescript@5.5.3) '@solana/web3.js': 1.98.0 '@ton/core': 0.59.0(@ton/crypto@3.3.0) '@ton/crypto': 3.3.0 @@ -11921,7 +12380,7 @@ packages: '@ethersproject/bytes': 5.8.0 '@initia/initia.js': 1.0.4(typescript@5.5.3) '@layerzerolabs/lz-definitions': 3.0.148 - '@mysten/sui': 1.39.0(typescript@5.5.3) + '@mysten/sui': 1.45.2(typescript@5.5.3) '@solana/web3.js': 1.98.0 '@ton/core': 0.59.0(@ton/crypto@3.3.0) '@ton/crypto': 3.3.0 @@ -11956,7 +12415,7 @@ packages: '@ethersproject/bytes': 5.8.0 '@initia/initia.js': 1.0.4(typescript@5.5.3) '@layerzerolabs/lz-definitions': 3.0.148 - '@mysten/sui': 1.39.0(typescript@5.5.3) + '@mysten/sui': 1.45.2(typescript@5.5.3) '@solana/web3.js': 1.98.0 '@ton/core': 0.59.0(@ton/crypto@3.3.0) '@ton/crypto': 3.3.0 @@ -12114,6 +12573,14 @@ packages: '@layerzerolabs/lz-definitions': 3.0.148 dev: true + /@layerzerolabs/oft-mint-burn-starknet@0.2.20(@layerzerolabs/protocol-starknet-v2@0.2.20): + resolution: {integrity: sha512-Hb2N+XZq7vak1JAiz/Wj5tP9qPrn8HzwfmIZw6QtFTIA9H24yby8Leew92CDh8wuHZn2Erc0bW8GRoqqxzGARg==} + peerDependencies: + '@layerzerolabs/protocol-starknet-v2': '>=0.2.0' + dependencies: + '@layerzerolabs/protocol-starknet-v2': 0.2.20 + dev: true + /@layerzerolabs/oft-v2-solana-sdk@3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(chai@4.4.1)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): resolution: {integrity: sha512-q2+OALy4F3cZE3Q2UGdmyiBLFROCfSo7X7rfbA8xPScdiIy6IP6p0nsDQeTGUbbMIbXzTOTrAaIPOiR+pfS4OA==} dependencies: @@ -12180,12 +12647,12 @@ packages: - utf-8-validate dev: false - /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-P06/a5+ixph0u1AQkDZ0P0oFaIAdfGPl/UezMfWXUpiWLth428RT0rrMR6qI7z6X1uxqlUFNIotz2ET1fyFcpQ==} dependencies: '@ethersproject/bytes': 5.7.0 '@layerzerolabs/lz-foundation': 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - '@layerzerolabs/lz-solana-sdk-v2': 3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@layerzerolabs/lz-solana-sdk-v2': 3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': 3.0.148 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 @@ -12213,12 +12680,12 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-ijbvj6/Gc4O4WLHfqnrBuKUtIhpaYws/ORj2apZFT1RKSgAX8CCJ9aZmn0ClamEG98i+PpXoroPPU46LMOMZyA==} dependencies: '@ethersproject/bytes': 5.7.0 '@layerzerolabs/lz-foundation': 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - '@layerzerolabs/lz-solana-sdk-v2': 3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@layerzerolabs/lz-solana-sdk-v2': 3.0.138(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': 3.0.148 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 @@ -12268,6 +12735,10 @@ packages: zod: 3.22.4 dev: true + /@layerzerolabs/protocol-starknet-v2@0.2.20: + resolution: {integrity: sha512-iWueRZJfVcL622AAo9XZQ/RmAq15cuNNFmRGAEc6YQUPJrOFkgmwrh2Bosxc2cHSQGIZFT+k/ZUS/nTLG4ttVg==} + dev: true + /@layerzerolabs/scan-client-v2@0.0.1(redaxios@0.5.1): resolution: {integrity: sha512-eugkDt4ajC5hqEHq23oTXMf28d5HrltapT7F5FIybluS8qAwJ5nc2Bbu9JRgUDdO8LQTQpi8AoVqnMbRumD4lA==} peerDependencies: @@ -12817,7 +13288,7 @@ packages: dependencies: '@metaplex-foundation/umi': 0.9.2 '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.0) - '@noble/curves': 1.8.1 + '@noble/curves': 1.9.6 '@solana/web3.js': 1.98.0 /@metaplex-foundation/umi-http-fetch@0.9.2(@metaplex-foundation/umi@0.9.2): @@ -12929,33 +13400,31 @@ packages: multiformats: 9.9.0 murmurhash3js-revisited: 3.0.0 - /@mysten/bcs@1.2.0: - resolution: {integrity: sha512-LuKonrGdGW7dq/EM6U2L9/as7dFwnhZnsnINzB/vu08Xfrj0qzWwpLOiXagAa5yZOPLK7anRZydMonczFkUPzA==} - dependencies: - bs58: 6.0.0 - - /@mysten/bcs@1.8.0: - resolution: {integrity: sha512-bDoLN1nN+XPONsvpNyNyqYHndM3PKWS419GLeRnbLoWyNm4bnyD1X4luEpJLLDq400hBuXiCan4RWjofvyTUIQ==} + /@mysten/bcs@1.9.2: + resolution: {integrity: sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ==} dependencies: '@mysten/utils': 0.2.0 '@scure/base': 1.2.6 - /@mysten/sui@1.39.0(typescript@5.5.3): - resolution: {integrity: sha512-tjH4oVAODO9JWPNvIBhAvorrwh7UfX5Lwf1oBjawnpk4sAIyajD8JYJUWXdI8o1H1519/5KEKaMT3ABAwTamQg==} + /@mysten/sui@1.45.2(typescript@5.5.3): + resolution: {integrity: sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==} engines: {node: '>=18'} dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) - '@mysten/bcs': 1.8.0 + '@mysten/bcs': 1.9.2 '@mysten/utils': 0.2.0 - '@noble/curves': 1.9.6 + '@noble/curves': 1.9.4 '@noble/hashes': 1.8.0 + '@protobuf-ts/grpcweb-transport': 2.11.1 + '@protobuf-ts/runtime': 2.11.1 + '@protobuf-ts/runtime-rpc': 2.11.1 '@scure/base': 1.2.6 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 gql.tada: 1.8.13(graphql@16.11.0)(typescript@5.5.3) graphql: 16.11.0 poseidon-lite: 0.2.1 - valibot: 0.36.0 + valibot: 1.2.0(typescript@5.5.3) transitivePeerDependencies: - '@gql.tada/svelte-support' - '@gql.tada/vue-support' @@ -12992,12 +13461,25 @@ packages: dependencies: '@noble/hashes': 1.3.3 + /@noble/curves@1.7.0: + resolution: {integrity: sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==} + engines: {node: ^14.21.3 || >=16} + dependencies: + '@noble/hashes': 1.6.0 + dev: true + /@noble/curves@1.8.1: resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} engines: {node: ^14.21.3 || >=16} dependencies: '@noble/hashes': 1.7.1 + /@noble/curves@1.9.4: + resolution: {integrity: sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==} + engines: {node: ^14.21.3 || >=16} + dependencies: + '@noble/hashes': 1.8.0 + /@noble/curves@1.9.6: resolution: {integrity: sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==} engines: {node: ^14.21.3 || >=16} @@ -13019,6 +13501,16 @@ packages: resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} engines: {node: '>= 16'} + /@noble/hashes@1.6.0: + resolution: {integrity: sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==} + engines: {node: ^14.21.3 || >=16} + dev: true + + /@noble/hashes@1.6.1: + resolution: {integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==} + engines: {node: ^14.21.3 || >=16} + dev: true + /@noble/hashes@1.7.1: resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} engines: {node: ^14.21.3 || >=16} @@ -13523,6 +14015,20 @@ packages: config-chain: 1.1.13 dev: true + /@protobuf-ts/grpcweb-transport@2.11.1: + resolution: {integrity: sha512-1W4utDdvOB+RHMFQ0soL4JdnxjXV+ddeGIUg08DvZrA8Ms6k5NN6GBFU2oHZdTOcJVpPrDJ02RJlqtaoCMNBtw==} + dependencies: + '@protobuf-ts/runtime': 2.11.1 + '@protobuf-ts/runtime-rpc': 2.11.1 + + /@protobuf-ts/runtime-rpc@2.11.1: + resolution: {integrity: sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==} + dependencies: + '@protobuf-ts/runtime': 2.11.1 + + /@protobuf-ts/runtime@2.11.1: + resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==} + /@protobufjs/aspromise@1.1.2: resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -13969,10 +14475,6 @@ packages: /@scure/base@1.1.5: resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==} - /@scure/base@1.2.1: - resolution: {integrity: sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ==} - dev: true - /@scure/base@1.2.4: resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} @@ -14039,6 +14541,13 @@ packages: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + /@scure/starknet@1.1.0: + resolution: {integrity: sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ==} + dependencies: + '@noble/curves': 1.7.0 + '@noble/hashes': 1.6.1 + dev: true + /@sentry/core@5.30.0: resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} engines: {node: '>=6'} @@ -14617,8 +15126,8 @@ packages: resolution: {integrity: sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA==} dependencies: '@babel/runtime': 7.25.6 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 + '@noble/curves': 1.9.6 + '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.5.0 bigint-buffer: 1.1.5 @@ -14687,6 +15196,14 @@ packages: - utf-8-validate dev: true + /@starknet-io/types-js@0.8.4: + resolution: {integrity: sha512-0RZ3TZHcLsUTQaq1JhDSCM8chnzO4/XNsSCozwDET64JK5bjFDIf2ZUkta+tl5Nlbf4usoU7uZiDI/Q57kt2SQ==} + dev: true + + /@starknet-io/types-js@0.9.2: + resolution: {integrity: sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg==} + dev: true + /@swc/core-darwin-arm64@1.4.0: resolution: {integrity: sha512-UTJ/Vz+s7Pagef6HmufWt6Rs0aUu+EJF4Pzuwvr7JQQ5b1DZeAAUeUtkUTFx/PvCbM8Xfw4XdKBUZfrIKCfW8A==} engines: {node: '>=10'} @@ -15952,6 +16469,16 @@ packages: web3-utils: 1.10.4 dev: true + /abi-wan-kanabi@2.2.4: + resolution: {integrity: sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg==} + hasBin: true + dependencies: + ansicolors: 0.3.2 + cardinal: 2.1.1 + fs-extra: 10.1.0 + yargs: 17.7.2 + dev: true + /abitype@1.0.8(typescript@5.5.3)(zod@3.22.4): resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==} peerDependencies: @@ -16692,7 +17219,7 @@ packages: /bip39@3.1.0: resolution: {integrity: sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==} dependencies: - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 /bl@1.2.3: resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} @@ -17146,6 +17673,14 @@ packages: tslib: 2.6.3 upper-case-first: 2.0.2 + /cardinal@2.1.1: + resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} + hasBin: true + dependencies: + ansicolors: 0.3.2 + redeyed: 2.1.1 + dev: true + /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -20812,7 +21347,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22493,6 +23028,27 @@ packages: supports-color: 8.1.1 dev: true + /jest@29.7.0(@types/node@18.18.14): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -22525,7 +23081,7 @@ packages: dependencies: '@jest/core': 29.7.0(ts-node@10.9.2) '@jest/types': 29.6.3 - import-local: 3.1.0 + import-local: 3.2.0 jest-cli: 29.7.0(@types/node@22.15.3)(ts-node@10.9.2) transitivePeerDependencies: - '@types/node' @@ -23043,6 +23599,10 @@ packages: dependencies: js-tokens: 4.0.0 + /lossless-json@4.3.0: + resolution: {integrity: sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==} + dev: true + /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -24799,6 +25359,12 @@ packages: resolution: {integrity: sha512-FSD2AmfdbkYwl7KDExYQlVvIrFz6Yd83pGfaGjBzM9F6rpq8g652Q4Yq5QD4c+nf4g2AgeElv1y+8ajUPiOYMg==} dev: false + /redeyed@2.1.1: + resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==} + dependencies: + esprima: 4.0.1 + dev: true + /reduce-flatten@2.0.0: resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} engines: {node: '>=6'} @@ -25764,6 +26330,22 @@ packages: dependencies: type-fest: 0.7.1 + /starknet@8.9.2: + resolution: {integrity: sha512-+dp+o2w67fV6JyVOVkYeM1Ec71aORHc/JrF4VHLlfeGee0nLilooCQLE2u6hUcSGQG2x2/fvzkxYpIN+k1JBvA==} + engines: {node: '>=22'} + dependencies: + '@noble/curves': 1.7.0 + '@noble/hashes': 1.6.1 + '@scure/base': 1.2.6 + '@scure/starknet': 1.1.0 + '@starknet-io/starknet-types-08': /@starknet-io/types-js@0.8.4 + '@starknet-io/starknet-types-09': /@starknet-io/types-js@0.9.2 + abi-wan-kanabi: 2.2.4 + lossless-json: 4.3.0 + pako: 2.1.0 + ts-mixer: 6.0.4 + dev: true + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -26505,6 +27087,10 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + dev: true + /ts-mocha@11.1.0(mocha@11.7.1)(ts-node@10.9.2): resolution: {integrity: sha512-yT7FfzNRCu8ZKkYvAOiH01xNma/vLq6Vit7yINKYFNVP8e5UyrYXSOMIipERTpzVKJQ4Qcos5bQo1tNERNZevQ==} engines: {node: '>= 6.X.X'} @@ -27215,9 +27801,6 @@ packages: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - /valibot@0.36.0: - resolution: {integrity: sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ==} - /valibot@0.37.0(typescript@5.5.3): resolution: {integrity: sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==} peerDependencies: @@ -27228,6 +27811,16 @@ packages: dependencies: typescript: 5.5.3 + /valibot@1.2.0(typescript@5.5.3): + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.5.3 + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: From 71a4a2b2c6c830edae14eee5eef320326f9d5a5b Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 27 Jan 2026 11:34:53 -0800 Subject: [PATCH 7/8] fix: handle non-EVM addresses in metadata-tools and ua-devtools-sui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add isNonEvmDeployment() to skip address checksumming for Sui/Starknet - Update setPeer() in ua-devtools-sui to handle addresses ≤32 bytes (Starknet is 31 bytes) --- .../metadata-tools/src/config-metadata.ts | 26 +++++++++++++++++-- packages/ua-devtools-sui/src/oft/sdk.ts | 8 +++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/metadata-tools/src/config-metadata.ts b/packages/metadata-tools/src/config-metadata.ts index 6ad2550694..45a03787be 100644 --- a/packages/metadata-tools/src/config-metadata.ts +++ b/packages/metadata-tools/src/config-metadata.ts @@ -112,6 +112,23 @@ function isSolanaDeployment(deployment: { chainKey: string; executor?: { pda?: s return deployment.chainKey.startsWith('solana') } +function isNonEvmDeployment(deployment: { chainKey: string }) { + return ['solana', 'sui', 'starknet', 'aptos', 'ton'].some((prefix) => deployment.chainKey.startsWith(prefix)) +} + +const maybeChecksumAddress = (address: string) => { + if (!address) { + return address + } + if (!address.startsWith('0x')) { + return address + } + if (address.length !== 42) { + return address + } + return getAddress(address) +} + function resolveExecutorForDeployment( customExecutor: string | undefined, deployment: { chainKey: string; executor?: { pda?: string; address?: string } }, @@ -167,8 +184,13 @@ function isBlocked(blockConfirmationsDefinition: BlockConfirmationsDefinition | ) } -const getLibraryAddress = (deployment: any, metadataKey: string) => - isSolanaDeployment(deployment) ? deployment[metadataKey].address : getAddress(deployment[metadataKey].address) +const getLibraryAddress = (deployment: any, metadataKey: string) => { + const address = deployment[metadataKey].address + if (isNonEvmDeployment(deployment)) { + return address + } + return maybeChecksumAddress(address) +} export async function translatePathwayToConfig( pathway: TwoWayConfig, diff --git a/packages/ua-devtools-sui/src/oft/sdk.ts b/packages/ua-devtools-sui/src/oft/sdk.ts index 93261caec1..d6969491fe 100644 --- a/packages/ua-devtools-sui/src/oft/sdk.ts +++ b/packages/ua-devtools-sui/src/oft/sdk.ts @@ -65,18 +65,18 @@ export class OFT extends OmniSDK implements IOApp { async setPeer(_eid: EndpointId, _peer: OmniAddress | null | undefined): Promise { const tx = new Transaction() - // Peer addresses must be 32 bytes (bytes32), so we need to pad EVM addresses (20 bytes) to 32 bytes + // Peer addresses must be 32 bytes (bytes32), so we need to pad shorter addresses to 32 bytes let peerBytes: Uint8Array if (_peer) { const rawBytes = fromHex(_peer) if (rawBytes.length === 32) { peerBytes = rawBytes - } else if (rawBytes.length === 20) { - // Pad EVM address (20 bytes) to 32 bytes with leading zeros + } else if (rawBytes.length <= 32) { + // Pad shorter addresses (20-byte EVM, 31-byte Starknet, etc.) to 32 bytes with leading zeros peerBytes = new Uint8Array(32) peerBytes.set(rawBytes, 32 - rawBytes.length) } else { - throw new Error(`Invalid peer address length: ${rawBytes.length}. Expected 20 or 32 bytes.`) + throw new Error(`Invalid peer address length: ${rawBytes.length}. Expected 32 bytes or less.`) } } else { peerBytes = new Uint8Array(32) From 99d2d96d3f2575c8773ae0590632bd272ef56ec6 Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 27 Jan 2026 15:01:54 -0800 Subject: [PATCH 8/8] chore: remove auto-generated CHANGELOGs from new packages and add changeset - Remove CHANGELOG.md from new sui/starknet packages (will be auto-generated on publish) - Add changeset for modified existing packages (devtools, metadata-tools, ua-devtools) --- .changeset/brave-clouds-swim.md | 7 +++++++ packages/devtools-starknet/CHANGELOG.md | 5 ----- packages/devtools-sui/CHANGELOG.md | 5 ----- packages/protocol-devtools-starknet/CHANGELOG.md | 5 ----- packages/protocol-devtools-sui/CHANGELOG.md | 5 ----- packages/ua-devtools-starknet/CHANGELOG.md | 5 ----- packages/ua-devtools-sui/CHANGELOG.md | 5 ----- 7 files changed, 7 insertions(+), 30 deletions(-) create mode 100644 .changeset/brave-clouds-swim.md delete mode 100644 packages/devtools-starknet/CHANGELOG.md delete mode 100644 packages/devtools-sui/CHANGELOG.md delete mode 100644 packages/protocol-devtools-starknet/CHANGELOG.md delete mode 100644 packages/protocol-devtools-sui/CHANGELOG.md delete mode 100644 packages/ua-devtools-starknet/CHANGELOG.md delete mode 100644 packages/ua-devtools-sui/CHANGELOG.md diff --git a/.changeset/brave-clouds-swim.md b/.changeset/brave-clouds-swim.md new file mode 100644 index 0000000000..4485c0bc24 --- /dev/null +++ b/.changeset/brave-clouds-swim.md @@ -0,0 +1,7 @@ +--- +"@layerzerolabs/devtools": patch +"@layerzerolabs/metadata-tools": patch +"@layerzerolabs/ua-devtools": patch +--- + +Add support for Sui and Starknet chain types in peer address handling diff --git a/packages/devtools-starknet/CHANGELOG.md b/packages/devtools-starknet/CHANGELOG.md deleted file mode 100644 index bd25c28af7..0000000000 --- a/packages/devtools-starknet/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# @layerzerolabs/devtools-starknet - -## 0.1.0 - -- Initial release. diff --git a/packages/devtools-sui/CHANGELOG.md b/packages/devtools-sui/CHANGELOG.md deleted file mode 100644 index ac0798a1f1..0000000000 --- a/packages/devtools-sui/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# @layerzerolabs/devtools-sui - -## 0.1.0 - -- Initial release. diff --git a/packages/protocol-devtools-starknet/CHANGELOG.md b/packages/protocol-devtools-starknet/CHANGELOG.md deleted file mode 100644 index 1181690e98..0000000000 --- a/packages/protocol-devtools-starknet/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# @layerzerolabs/protocol-devtools-starknet - -## 0.1.0 - -- Initial release. diff --git a/packages/protocol-devtools-sui/CHANGELOG.md b/packages/protocol-devtools-sui/CHANGELOG.md deleted file mode 100644 index dbbf0d5cfa..0000000000 --- a/packages/protocol-devtools-sui/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# @layerzerolabs/protocol-devtools-sui - -## 0.1.0 - -- Initial release. diff --git a/packages/ua-devtools-starknet/CHANGELOG.md b/packages/ua-devtools-starknet/CHANGELOG.md deleted file mode 100644 index cbb231d0ce..0000000000 --- a/packages/ua-devtools-starknet/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# @layerzerolabs/ua-devtools-starknet - -## 0.1.0 - -- Initial release. diff --git a/packages/ua-devtools-sui/CHANGELOG.md b/packages/ua-devtools-sui/CHANGELOG.md deleted file mode 100644 index 905faf0cc2..0000000000 --- a/packages/ua-devtools-sui/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# @layerzerolabs/ua-devtools-sui - -## 0.1.0 - -- Initial release.