Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/LayerZero-Labs/devtools int…
Browse files Browse the repository at this point in the history
…o feat/oft-alt-example
  • Loading branch information
shankars99 committed Nov 20, 2024
2 parents b5a569f + 30e01b8 commit 9f51c6e
Show file tree
Hide file tree
Showing 20 changed files with 892 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-peas-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@layerzerolabs/devtools-evm-hardhat": patch
---

Updating devtools dependency
4 changes: 2 additions & 2 deletions packages/devtools-evm-hardhat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@ethersproject/wallet": "^5.7.0",
"@layerzerolabs/devtools": "~0.4.0",
"@layerzerolabs/devtools": "~0.4.1",
"@layerzerolabs/devtools-evm": "~1.0.0",
"@layerzerolabs/io-devtools": "~0.1.13",
"@layerzerolabs/lz-definitions": "^3.0.12",
Expand Down Expand Up @@ -83,7 +83,7 @@
"@ethersproject/abstract-signer": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@layerzerolabs/devtools": "~0.4.0",
"@layerzerolabs/devtools": "~0.4.1",
"@layerzerolabs/devtools-evm": "~1.0.0",
"@layerzerolabs/io-devtools": "~0.1.13",
"@layerzerolabs/lz-definitions": "^3.0.12",
Expand Down
3 changes: 3 additions & 0 deletions packages/devtools-ton/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.turbo
dist
node_modules
3 changes: 3 additions & 0 deletions packages/devtools-ton/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.json"
}
11 changes: 11 additions & 0 deletions packages/devtools-ton/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true
},
"transform": {
"legacyDecorator": true
}
}
}
19 changes: 19 additions & 0 deletions packages/devtools-ton/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<p align="center">
<a href="https://layerzero.network">
<img alt="LayerZero" style="max-width: 500px" src="https://d3a2dpnnrypp5h.cloudfront.net/bridge-app/lz.png"/>
</a>
</p>

<h1 align="center">@layerzerolabs/devtools-ton</h1>

<!-- The badges section -->
<p align="center">
<!-- Shields.io NPM published package version -->
<a href="https://www.npmjs.com/package/@layerzerolabs/devtools-ton"><img alt="NPM Version" src="https://img.shields.io/npm/v/@layerzerolabs/devtools-ton"/></a>
<!-- Shields.io NPM downloads -->
<a href="https://www.npmjs.com/package/@layerzerolabs/devtools-ton"><img alt="Downloads" src="https://img.shields.io/npm/dm/@layerzerolabs/devtools-ton"/></a>
<!-- Shields.io license badge -->
<a href="https://www.npmjs.com/package/@layerzerolabs/devtools-ton"><img alt="NPM License" src="https://img.shields.io/npm/l/@layerzerolabs/devtools-ton"/></a>
</p>

Utilities for working with LayerZero TON contracts.
16 changes: 16 additions & 0 deletions packages/devtools-ton/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Config } from 'jest'

const config: Config = {
cache: false,
reporters: [['github-actions', { silent: false }], 'default'],
testEnvironment: 'node',
testTimeout: 60_000,
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
}

export default config
63 changes: 63 additions & 0 deletions packages/devtools-ton/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@layerzerolabs/devtools-ton",
"version": "0.0.1",
"description": "Developer utilities for working with LayerZero TON contracts",
"repository": {
"type": "git",
"url": "git+https://github.com/LayerZero-Labs/devtools.git",
"directory": "packages/devtools-ton"
},
"license": "MIT",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"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"
},
"devDependencies": {
"@layerzerolabs/devtools": "~0.4.1",
"@layerzerolabs/io-devtools": "~0.1.13",
"@layerzerolabs/lz-definitions": "^3.0.12",
"@swc/core": "^1.4.0",
"@swc/jest": "^0.2.36",
"@ton/core": "^0.59.0",
"@ton/crypto": "^3.3.0",
"@ton/ton": "^15.1.0",
"@types/jest": "^29.5.12",
"fast-check": "^3.16.0",
"jest": "^29.7.0",
"ton-crypto": "^3.2.0",
"ts-node": "^10.9.2",
"tslib": "~2.6.2",
"tsup": "~8.0.1",
"typescript": "^5.4.4"
},
"peerDependencies": {
"@layerzerolabs/devtools": "~0.4.1",
"@layerzerolabs/io-devtools": "~0.1.13",
"@layerzerolabs/lz-definitions": "^3.0.12",
"@ton/core": "^0.59.0",
"@ton/crypto": "^3.3.0",
"@ton/ton": "^15.1.0"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/devtools-ton/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './transactions'
3 changes: 3 additions & 0 deletions packages/devtools-ton/src/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './serde'
export * from './signer'
export * from './state'
91 changes: 91 additions & 0 deletions packages/devtools-ton/src/transactions/serde.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { createModuleLogger } from '@layerzerolabs/io-devtools'
import {
beginCell,
Cell,
loadMessage,
loadMessageRelaxed,
type Message,
storeMessage,
storeMessageRelaxed,
type MessageRelaxed,
} from '@ton/core'

/**
* Serializes a Message object into a string that can be passed to OmniTransaction data
*
* @param {Message} message
* @returns {string} Base64 serialized message string
*/
export const serializeMessage = (message: Message): string =>
beginCell().store(storeMessage(message)).endCell().toBoc().toString('base64')

/**
* Serializes a MessageRelaxed object into a string that can be passed to OmniTransaction data
*
* @param {MessageRelaxed} message
* @returns {string} Base64 serialized message string
*/
export const serializeMessageRelaxed = (message: MessageRelaxed): string =>
messageRelaxedToCell(message).toBoc().toString('base64')

/**
* Deserializes a Message object from Base64 serialized representation
*
* @param {string} data
* @returns {Message}
*/
export const deserializeMessage = (data: string): Message => loadMessage(Cell.fromBase64(data).beginParse())

/**
* Deserializes a MessageRelaxed object from Base64 serialized representation
*
* @param {string} data
* @returns {MessageRelaxed}
*/
export const deserializeMessageRelaxed = (data: string): MessageRelaxed =>
loadMessageRelaxed(Cell.fromBase64(data).beginParse())

/**
* Tries to deserialize Base64 serialized data into a Message or MessageRelaxed object
*
* @param {string} data
* @returns {Message | MessageRelaxed}
*/
export const deserialize = (data: string): Message | MessageRelaxed => {
const logger = createModuleLogger(`devtools-ton/transactions/serde:deserialize`)

try {
return deserializeMessageRelaxed(data)
} catch (errorMessageRelaxed) {
logger.verbose(`Failed to deserialize data as MessageRelaxed, trying Message: ${errorMessageRelaxed}`)

try {
return deserializeMessage(data)
} catch (errorMessage) {
logger.verbose(`Failed to deserialize data as Message: ${errorMessageRelaxed}`)

throw new Error(
`Failed to deserialize data.\n\nDeserializing as MessageRelaxed:\n\n${errorMessageRelaxed}\n\nDeserializing as Message:\n\n${errorMessage}`
)
}
}
}

/**
* Helper function for partial serialization,
* mostly to work around jest expectation issues with Message equality
*
* @param {Message} message
* @returns {Cell}
*/
export const messageToCell = (message: Message): Cell => beginCell().store(storeMessage(message)).endCell()

/**
* Helper function for partial serialization,
* mostly to work around jest expectation issues with MessageRelaxed equality
*
* @param {MessageRelaxed} message
* @returns {Cell}
*/
export const messageRelaxedToCell = (message: MessageRelaxed): Cell =>
beginCell().store(storeMessageRelaxed(message)).endCell()
123 changes: 123 additions & 0 deletions packages/devtools-ton/src/transactions/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {
type OmniPoint,
OmniSignerBase,
type OmniTransaction,
type OmniTransactionResponse,
type OmniSigner,
AsyncRetriable,
} from '@layerzerolabs/devtools'
import type { EndpointId } from '@layerzerolabs/lz-definitions'
import type { Cell, OpenedContract, Contract, ContractProvider, MessageRelaxed } from '@ton/core'
import { TonClient } from '@ton/ton'
import type { KeyPair } from '@ton/crypto'
import { deserializeMessageRelaxed } from './serde'
import assert from 'assert'
import { createIsCellInTransaction, hasTransactionBounced, isTransactionSuccessful } from './state'
import { createModuleLogger, Logger } from '@layerzerolabs/io-devtools'

export interface IWalletContractCreateTransferArgs {
seqno: number
secretKey: Buffer
messages: MessageRelaxed[]
}

export interface IWalletContract extends Contract {
/**
* Get Wallet Seqno
*/
getSeqno(provider: ContractProvider): Promise<number>

/**
* Create transfer cell
* @param args
*/
createTransfer(args: IWalletContractCreateTransferArgs): Promise<Cell> | Cell

/**
* Send signed transfer
*/
send(provider: ContractProvider, message: Cell): Promise<void>
}

export class OmniSignerTON<TWalletContract extends IWalletContract> extends OmniSignerBase implements OmniSigner {
// Due to generic type parameter resolution limitations, this needs to be typed as OpenedContract<IWalletContract>
// rather than OpenedContract<TWalletContract>
//
// Using TWalletContract as the type parameter breaks the type inference for the OpenedContract type
protected readonly openWallet: OpenedContract<IWalletContract>

constructor(
eid: EndpointId,
public readonly keyPair: KeyPair,
public readonly endpoint: string,
public readonly wallet: TWalletContract,
public readonly client: TonClient = new TonClient({ endpoint }),
protected readonly logger: Logger = createModuleLogger('OmniSignerTON')
) {
super(eid)

this.openWallet = client.open(wallet)
}

override getPoint(): OmniPoint | Promise<OmniPoint> {
return { eid: this.eid, address: this.keyPair.publicKey.toString('base64') }
}

override sign(_transaction: OmniTransaction): Promise<string> {
throw new Error('Method not implemented.')
}

override async signAndSend(omniTransaction: OmniTransaction): Promise<OmniTransactionResponse> {
const seqno = await this.openWallet.getSeqno()
const cell = await this.wallet.createTransfer({
seqno,
secretKey: this.keyPair.secretKey,
messages: [deserializeMessageRelaxed(omniTransaction.data)],
})

await this.openWallet.send(cell)

const transaction = await this.waitForCellSubmitted(cell)
const transactionHash = transaction.hash().toString('base64')

return {
transactionHash,
// This wait function does not really wait at this moment, it just gets the transaction state again
// and checks whether the phases have completed successfully
//
// API V3 is required to investigate transaction traces for pending and finalized states.
//
// The guarantee that we rely on is that if two transactions have been submitted in order,
// their messages will be executed in order even if the later transactions don't wait for the earlier ones to finish
wait: async () => {
const transactionState = await this.client.getTransaction(
this.wallet.address,
transaction.lt.toString(),
transactionHash
)
assert(transactionState != null, `Transaction '${transactionHash}' missing from the API`)
assert(!hasTransactionBounced(transactionState), `Transaction '${transactionHash}' has bounced`)
assert(isTransactionSuccessful(transactionState), `Transaction '${transactionHash}' has not succeeded`)

return { transactionHash }
},
}
}

@AsyncRetriable({
// We'll use the AsyncRetriable with infinite retries to provide us with a exponential backoff retry logic
enabled: true,
numAttempts: Number.POSITIVE_INFINITY,
maxDelay: 1_000,
})
protected async waitForCellSubmitted(cell: Cell, limit: number = 100) {
const transactions = await this.client.getTransactions(this.wallet.address, {
limit,
})

const transaction = transactions.find(createIsCellInTransaction(cell))
assert(transaction != null, `Failed to locate cell ${cell.toString()} among the last ${limit} transactions`)

return transaction
}
}
Loading

0 comments on commit 9f51c6e

Please sign in to comment.