import { array } from '@metaplex-foundation/beet' import { AccountInfo, AccountMeta, Commitment, Connection, GetAccountInfoConfig, PublicKey, TransactionInstruction, } from '@solana/web3.js' import { arrayify } from '@layerzerolabs/lz-utilities' import { Packet, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' import { deriveLzComposeTypesAccountsPDA, deriveLzReceiveTypesAccountsPDA } from '.' import { AddressType, LzComposeParams, LzComposeParamsBeet, LzReceiveAccountBeet, LzReceiveParams, LzReceiveParamsBeet, } from './types' import { instructionDiscriminator, simulateTransaction } from './utility' import { ComposeSentEvent } from '../generated/solita/endpoint/events' /** * Receives a LayerZero message. * * @param {Connection} connection - The Solana connection object. * @param {PublicKey} payer - The public key of the payer. * @param {Packet} packet - The packet containing the message. * @param {Uint8Array} [callerParams] - Optional caller parameters. * @param {Commitment | GetAccountInfoConfig} [commitmentOrConfig] - Optional commitment or configuration. * * @returns {Promise} The transaction instruction. */ export async function lzReceive( connection: Connection, payer: PublicKey, packet: Pick, callerParams: Uint8Array = Uint8Array.from([0, 0]), commitmentOrConfig: Commitment | GetAccountInfoConfig = 'confirmed' ): Promise { const { message: message_, sender: sender, srcEid, guid, receiver: receiver_ } = packet const receiver = new PublicKey(addressToBytes32(receiver_)) const message = arrayify(message_) const params: LzReceiveParams = { srcEid: srcEid, sender: Array.from(arrayify(sender)), guid: Array.from(arrayify(guid)), message, callerParams, nonce: parseInt(packet.nonce), } const receiverInfo = await connection.getParsedAccountInfo(receiver, commitmentOrConfig) const receiverProgram = new PublicKey((receiverInfo.value as AccountInfo).owner) const accounts = await getLzReceiveAccounts( connection, payer, receiver, receiverProgram, params, commitmentOrConfig ) const MSG_TYPE_OFFSET = 0 // const msgType: string = message[MSG_TYPE_OFFSET].toString() // if (msgType !== MessageType.COMPOSED_TYPE.toString() && msgType !== MessageType.VANILLA.toString()) { // throw new Error(`Invalid message type ${msgType}`) // } const [data] = LzReceiveParamsBeet.serialize(params) return new TransactionInstruction({ programId: receiverProgram, keys: accounts, data: Buffer.concat([instructionDiscriminator('lz_receive'), data]), }) } /** * Composes a LayerZero message. * * @param {Connection} connection - The Solana connection object. * @param {PublicKey} payer - The public key of the payer. * @param {ComposeSentEvent} event - The event containing the message details. * @param {Uint8Array} [extraData] - Optional extra data. * @param {Commitment | GetAccountInfoConfig} [commitmentOrConfig] - Optional commitment or configuration. * * @returns {Promise} The transaction instruction. */ export async function lzCompose( connection: Connection, payer: PublicKey, event: ComposeSentEvent, extraData: Uint8Array = Uint8Array.from([0, 0]), commitmentOrConfig?: Commitment | GetAccountInfoConfig ): Promise { const { to, from, guid, index, message } = event const params: LzComposeParams = { from, to, guid, index, message, extraData, } const accountInfo = await connection.getAccountInfo(to, commitmentOrConfig) if (!accountInfo) { throw new Error(`Account not found: ${to.toBase58()}`) } const programId = accountInfo.owner const accounts = await getLzComposeAccountMeta(connection, payer, to, programId, params, commitmentOrConfig) const [data] = LzComposeParamsBeet.serialize(params) return new TransactionInstruction({ programId, keys: accounts, data: Buffer.concat([instructionDiscriminator('lz_compose'), data]), }) } /// LzReceiveTypes instruction provides a list of accounts that are used in the LzReceive /// instruction. The list of accounts required by this LzReceiveTypes instruction can be found /// from the specific PDA account that is generated by the LZ_RECEIVE_TYPES_SEED. /** * Retrieves the accounts required for the LzReceive instruction. * * @param {Connection} connection - The Solana connection object. * @param {PublicKey} payer - The public key of the payer. * @param {PublicKey} receiver - The public key of the receiver. * @param {PublicKey} receiverProgram - The public key of the receiver program. * @param {LzReceiveParams} params - The parameters for the LzReceive instruction. * @param {Commitment | GetAccountInfoConfig} [commitmentOrConfig] - Optional commitment or configuration. * * @returns {Promise} The list of account metadata. */ export async function getLzReceiveAccounts( connection: Connection, payer: PublicKey, receiver: PublicKey, receiverProgram: PublicKey, params: LzReceiveParams, commitmentOrConfig: Commitment | GetAccountInfoConfig = 'confirmed' ): Promise { const lzReceiveTypesAccounts = await (async (): Promise => { const [lzReceiveTypesAccountsPDA] = deriveLzReceiveTypesAccountsPDA(receiverProgram, receiver) // Get the lzReceiveTypesAccounts. Deserialize the AccountInfo.data to get the lzReceiveTypesAccounts. const info = await connection.getAccountInfo(lzReceiveTypesAccountsPDA, commitmentOrConfig) const accounts = [] if (info) { const buffer = Buffer.from(info.data) const len = buffer.length - 8 if (len % 32 !== 0) { throw new Error( `Invalid length of AccountInfo.data. The length must be a multiple of 32 plus 8.(n*32+8). Current length is ${buffer.length}` ) } for (let i = 8; i < len; i += 32) { const address = AddressType.read(buffer, i) accounts.push({ pubkey: new PublicKey(address), isSigner: false, isWritable: false, }) } } return accounts })() const [data] = LzReceiveParamsBeet.serialize(params) const lzReceiveTypesIx = new TransactionInstruction({ programId: receiverProgram, keys: lzReceiveTypesAccounts, data: Buffer.concat([instructionDiscriminator('lz_receive_types'), data]), }) const response = await simulateTransaction(connection, [lzReceiveTypesIx], receiverProgram, payer) const keys: AccountMeta[] = retrieveAccountFromSimulatedResp(response, payer) return keys } /** * Retrieves the accounts required for the LzCompose instruction. * * @param {Connection} connection - The Solana connection object. * @param {PublicKey} payer - The public key of the payer. * @param {PublicKey} to - The public key of the recipient. * @param {PublicKey} composerProgram - The public key of the composer program. * @param {LzComposeParams} params - The parameters for the LzCompose instruction. * @param {Commitment | GetAccountInfoConfig} [commitmentOrConfig] - Optional commitment or configuration. * * @returns {Promise} The list of account metadata. */ export async function getLzComposeAccountMeta( connection: Connection, payer: PublicKey, to: PublicKey, composerProgram: PublicKey, params: LzComposeParams, commitmentOrConfig: Commitment | GetAccountInfoConfig = 'confirmed' ): Promise { const lzComposeTypesAccounts = await (async (): Promise => { const [lzComposeTypesAccountsPDA] = deriveLzComposeTypesAccountsPDA(composerProgram, to) // Get the lzReceiveTypesAccounts. Deserialize the AccountInfo.data to get the lzComposeTypesAccounts. const info = await connection.getAccountInfo(lzComposeTypesAccountsPDA, commitmentOrConfig) const accounts = [] if (info) { const buffer = Buffer.from(info.data) const len = buffer.length - 8 if (len % 32 !== 0) { throw new Error( `Invalid length of AccountInfo.data. The length must be a multiple of 32 plus 8.(n*32+8). Current length is ${buffer.length}` ) } for (let i = 8; i < len; i += 32) { const address = AddressType.read(buffer, i) accounts.push({ pubkey: new PublicKey(address), isSigner: false, isWritable: false, }) } } return accounts })() const [data] = LzComposeParamsBeet.serialize(params) const lzComposeTypesIx = new TransactionInstruction({ programId: composerProgram, keys: lzComposeTypesAccounts, data: Buffer.concat([instructionDiscriminator('lz_compose_types'), data]), }) const response = await simulateTransaction(connection, [lzComposeTypesIx], composerProgram, payer) const keys: AccountMeta[] = retrieveAccountFromSimulatedResp(response, payer) return keys } /** * Retrieves account metadata from a simulated transaction response. * * @param {Buffer} resp - The response buffer. * @param {PublicKey} [payer] - Optional public key of the payer. * * @returns {AccountMeta[]} The list of account metadata. */ function retrieveAccountFromSimulatedResp(resp: Buffer, payer?: PublicKey): AccountMeta[] { const result = array(LzReceiveAccountBeet) const fixedBeet = result.toFixedFromData(resp, 0) const results = fixedBeet.read(resp, 0) return results.map((r) => { if (r.pubkey.toBase58() == PublicKey.default.toBase58() && r.isSigner) { if (!payer) throw new Error('payer is required') return { pubkey: payer, isSigner: true, isWritable: r.isWritable, } satisfies AccountMeta } else { return { pubkey: r.pubkey, isSigner: r.isSigner, isWritable: r.isWritable, } satisfies AccountMeta } }) }