import { AccountInfo, AccountMeta, Commitment, Connection, GetAccountInfoConfig, PublicKey } from '@solana/web3.js' import invariant from 'tiny-invariant' import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' import { EndpointProgram, SimpleMessageLibProgram, UlnProgram } from '.' import { DVN, accounts as DvnAccounts } from './dvn' import { DefaultMessageLib } from './endpoint' import { Executor, accounts as ExecutorAccounts } from './executor' /** * Interface representing keyed account information. */ interface KeyedAccountInfo { /** * The account ID. */ accountId: PublicKey /** * The account information. */ accountInfo: AccountInfo | null } /** * Enum representing quote or send actions. */ enum QuoteOrSend { /** * Quote action. */ Quote, /** * Send action. */ Send, } /** * Class representing a helper for sending transactions. */ export class SendHelper { private endpoint: EndpointProgram.Endpoint private uln: UlnProgram.Uln private simpleMsgLib: SimpleMessageLibProgram.SimpleMessageLib private accounts = new Map | null>() /** * Creates an instance of the SendHelper class. * * @param {PublicKey} [endpointProgram=EndpointProgram.PROGRAM_ID] - The endpoint program public key. * @param {PublicKey} [ulnProgram=UlnProgram.PROGRAM_ID] - The ULN program public key. * @param {PublicKey} [simpleMsgLibProgram=SimpleMessageLibProgram.PROGRAM_ID] - The simple message library program public key. */ constructor( public readonly endpointProgram: PublicKey = EndpointProgram.PROGRAM_ID, public readonly ulnProgram: PublicKey = UlnProgram.PROGRAM_ID, public readonly simpleMsgLibProgram: PublicKey = SimpleMessageLibProgram.PROGRAM_ID ) { this.endpoint = new EndpointProgram.Endpoint(endpointProgram) this.uln = new UlnProgram.Uln(ulnProgram) this.simpleMsgLib = new SimpleMessageLibProgram.SimpleMessageLib(simpleMsgLibProgram) } /** * Fetches multiple account information. * * @param {Connection} connection - The connection to the Solana cluster. * @param {PublicKey[]} keys - The public keys of the accounts. * @param {Commitment | GetAccountInfoConfig} [commitment] - The commitment level or account info configuration. * @returns {Promise<(AccountInfo | null)[]>} A promise that resolves to an array of account information. */ private async getMultipleAccountsInfo( connection: Connection, keys: PublicKey[], commitment?: Commitment | GetAccountInfoConfig ): Promise<(AccountInfo | null)[]> { // check if all accounts are already fetched const missingKeys = keys.filter((key) => !this.accounts.has(key.toBase58())) // fetch missing accounts if (missingKeys.length > 0) { const infos = await connection.getMultipleAccountsInfo(missingKeys, commitment) infos.forEach((info, i) => { this.accounts.set(missingKeys[i].toBase58(), info) }) } // return all accounts return keys.map((key) => this.accounts.get(key.toBase58())!) } /** * Gets the account metadata for the quote instruction. * * @param {Connection} connection - The connection to the Solana cluster. * @param {PublicKey} payer - The payer public key. * @param {PublicKey} sender - The sender public key. * @param {number} dstEid - The destination endpoint ID. * @param {string} receiver - The receiver address. * @param {Commitment | GetAccountInfoConfig} [commitmentOrConfig='confirmed'] - The commitment level or account info configuration. * @returns {Promise} A promise that resolves to an array of account metadata. */ async getQuoteAccounts( connection: Connection, payer: PublicKey, sender: PublicKey, dstEid: number, receiver: string, commitmentOrConfig: Commitment | GetAccountInfoConfig = 'confirmed' ): Promise { const [sendLibConfig] = this.endpoint.deriver.sendLibraryConfig(sender, dstEid) const [defaultSendLibConfig] = this.endpoint.deriver.defaultSendLibraryConfig(dstEid) const [simpleMsgLib] = this.simpleMsgLib.deriver.messageLib() const [uln] = this.uln.deriver.messageLib() const [ulnDefaultSendConfig] = this.uln.deriver.defaultSendConfig(dstEid) const [ulnSendConfig] = this.uln.deriver.sendConfig(dstEid, sender) const [ sendLibConfigBuf, defaultSendLibConfigBuf, _simpleMsgLibBuf, ulnBuf, ulnDefaultSendConfigBuf, ulnSendConfigBuf, ] = await this.getMultipleAccountsInfo( connection, [sendLibConfig, defaultSendLibConfig, simpleMsgLib, uln, ulnDefaultSendConfig, ulnSendConfig], commitmentOrConfig ) invariant(defaultSendLibConfigBuf && sendLibConfigBuf, 'endpoint send library not initialized') const [sendLibConfigInfo] = EndpointProgram.accounts.SendLibraryConfig.fromAccountInfo(sendLibConfigBuf, 0) const [defaultSendLibConfigInfo] = EndpointProgram.accounts.SendLibraryConfig.fromAccountInfo( defaultSendLibConfigBuf, 0 ) const msgLib = sendLibConfigInfo.messageLib.toString() === DefaultMessageLib.toString() ? defaultSendLibConfigInfo.messageLib : sendLibConfigInfo.messageLib let msgLibProgram = this.ulnProgram if (msgLib.toBase58() === this.simpleMsgLib.deriver.messageLib()[0].toBase58()) { msgLibProgram = this.simpleMsgLibProgram return this.getEndpointAccounts(msgLibProgram, msgLib, sender, dstEid, receiver, QuoteOrSend.Quote).concat( this.getSimpleMsgLibAccounts(payer, QuoteOrSend.Quote) ) } if (msgLib.toBase58() !== this.uln.deriver.messageLib()[0].toBase58()) { throw new Error('Invalid message library') } return this.getEndpointAccounts(msgLibProgram, msgLib, sender, dstEid, receiver, QuoteOrSend.Quote).concat( await this.getUlnAccounts( connection, payer, { accountId: uln, accountInfo: ulnBuf, }, { accountId: ulnDefaultSendConfig, accountInfo: ulnDefaultSendConfigBuf, }, { accountId: ulnSendConfig, accountInfo: ulnSendConfigBuf, }, QuoteOrSend.Quote, commitmentOrConfig ) ) } /** * Gets the account metadata for the send instruction. * * @param {Connection} connection - The connection to the Solana cluster. * @param {PublicKey} payer - The payer public key. * @param {PublicKey} sender - The sender public key. * @param {number} dstEid - The destination endpoint ID. * @param {string} receiver - The receiver address. * @param {Commitment | GetAccountInfoConfig} [commitmentOrConfig='confirmed'] - The commitment level or account info configuration. * @returns {Promise} A promise that resolves to an array of account metadata. */ async getSendAccounts( connection: Connection, payer: PublicKey, sender: PublicKey, dstEid: number, receiver: string, commitmentOrConfig: Commitment | GetAccountInfoConfig = 'confirmed' ): Promise { const [sendLibConfig] = this.endpoint.deriver.sendLibraryConfig(sender, dstEid) const [defaultSendLibConfig] = this.endpoint.deriver.defaultSendLibraryConfig(dstEid) const [simpleMsgLib] = this.simpleMsgLib.deriver.messageLib() const [uln] = this.uln.deriver.messageLib() const [ulnDefaultSendConfig] = this.uln.deriver.defaultSendConfig(dstEid) const [ulnSendConfig] = this.uln.deriver.sendConfig(dstEid, sender) const [ sendLibConfigBuf, defaultSendLibConfigBuf, _simpleMsgLibBuf, ulnBuf, ulnDefaultSendConfigBuf, ulnSendConfigBuf, ] = await this.getMultipleAccountsInfo( connection, [sendLibConfig, defaultSendLibConfig, simpleMsgLib, uln, ulnDefaultSendConfig, ulnSendConfig], commitmentOrConfig ) invariant(defaultSendLibConfigBuf && sendLibConfigBuf, 'endpoint send library not initialized') const [sendLibConfigInfo] = EndpointProgram.accounts.SendLibraryConfig.fromAccountInfo(sendLibConfigBuf, 0) const [defaultSendLibConfigInfo] = EndpointProgram.accounts.SendLibraryConfig.fromAccountInfo( defaultSendLibConfigBuf, 0 ) const msgLib = sendLibConfigInfo.messageLib.toString() === DefaultMessageLib.toString() ? defaultSendLibConfigInfo.messageLib : sendLibConfigInfo.messageLib let msgLibProgram = this.ulnProgram if (msgLib.toBase58() === this.simpleMsgLib.deriver.messageLib()[0].toBase58()) { msgLibProgram = this.simpleMsgLibProgram return this.getEndpointAccounts(msgLibProgram, msgLib, sender, dstEid, receiver, QuoteOrSend.Send).concat( this.getSimpleMsgLibAccounts(payer, QuoteOrSend.Send) ) } if (msgLib.toBase58() !== this.uln.deriver.messageLib()[0].toBase58()) { throw new Error('Invalid message library') } return this.getEndpointAccounts(msgLibProgram, msgLib, sender, dstEid, receiver, QuoteOrSend.Send).concat( await this.getUlnAccounts( connection, payer, { accountId: uln, accountInfo: ulnBuf, }, { accountId: ulnDefaultSendConfig, accountInfo: ulnDefaultSendConfigBuf, }, { accountId: ulnSendConfig, accountInfo: ulnSendConfigBuf, }, QuoteOrSend.Send, commitmentOrConfig ) ) } /** * Gets the account metadata for the endpoint. * * @param {PublicKey} msgLibProgram - The message library program public key. * @param {PublicKey} msgLib - The message library public key. * @param {PublicKey} sender - The sender public key. * @param {number} dstEid - The destination endpoint ID. * @param {string} receiver - The receiver address. * @param {QuoteOrSend} quoteOrSend - The quote or send action. * @returns {AccountMeta[]} An array of account metadata. */ private getEndpointAccounts( msgLibProgram: PublicKey, msgLib: PublicKey, sender: PublicKey, dstEid: number, receiver: string, quoteOrSend: QuoteOrSend ): AccountMeta[] { const [sendLibraryConfig] = this.endpoint.deriver.sendLibraryConfig(sender, dstEid) const [defaultSendLibraryConfig] = this.endpoint.deriver.defaultSendLibraryConfig(dstEid) const [sendLibraryInfo] = this.endpoint.deriver.messageLibraryInfo(msgLib) const [nonce] = this.endpoint.deriver.nonce(sender, dstEid, addressToBytes32(receiver)) let accounts: AccountMeta[] if (quoteOrSend === QuoteOrSend.Quote) { accounts = EndpointProgram.instructions.createQuoteInstructionAccounts( { sendLibraryProgram: msgLibProgram, sendLibraryConfig, defaultSendLibraryConfig, sendLibraryInfo, endpoint: this.endpoint.deriver.setting()[0], nonce, } satisfies EndpointProgram.instructions.QuoteInstructionAccounts, this.endpointProgram ) } else { accounts = EndpointProgram.instructions.createSendInstructionAccounts( { sender, //signer sendLibraryProgram: msgLibProgram, sendLibraryConfig, defaultSendLibraryConfig, sendLibraryInfo, endpoint: this.endpoint.deriver.setting()[0], nonce, program: this.endpointProgram, eventAuthority: this.endpoint.eventAuthorityPDA, } satisfies EndpointProgram.instructions.SendInstructionAccounts, this.endpoint.program ) } accounts.forEach((item: AccountMeta) => { item.isSigner = false }) return [ { pubkey: this.endpoint.program, isSigner: false, isWritable: false, }, ].concat(accounts) } /** * Gets the account metadata for the simple message library. * * @param {PublicKey} payer - The payer public key. * @param {QuoteOrSend} quoteOrSend - The quote or send action. * @returns {AccountMeta[]} An array of account metadata. */ private getSimpleMsgLibAccounts(payer: PublicKey, quoteOrSend: QuoteOrSend): AccountMeta[] { const [msgLib] = this.simpleMsgLib.deriver.messageLib() let accounts: AccountMeta[] if (quoteOrSend === QuoteOrSend.Quote) { accounts = SimpleMessageLibProgram.instructions.createQuoteInstructionAccounts( { endpoint: PublicKey.default, // useless messageLib: msgLib, } satisfies SimpleMessageLibProgram.instructions.QuoteInstructionAccounts, this.simpleMsgLibProgram ) } else { accounts = SimpleMessageLibProgram.instructions.createSendInstructionAccounts( { endpoint: PublicKey.default, // useless messageLib: msgLib, payer, } satisfies SimpleMessageLibProgram.instructions.SendInstructionAccounts, this.simpleMsgLibProgram ) } accounts.forEach((key) => { if (!payer.equals(key.pubkey)) { key.isSigner = false } }) return accounts.slice(1) } /** * Gets the account metadata for the ULN. * * @param {Connection} connection - The connection to the Solana cluster. * @param {PublicKey} payer - The payer public key. * @param {KeyedAccountInfo} ulnInfo - The ULN account information. * @param {KeyedAccountInfo} ulnDefaultSendConfigInfo - The ULN default send configuration account information. * @param {KeyedAccountInfo} ulnSendConfigInfo - The ULN send configuration account information. * @param {QuoteOrSend} quoteOrSend - The quote or send action. * @param {Commitment | GetAccountInfoConfig} [commitment] - The commitment level or account info configuration. * @returns {Promise} A promise that resolves to an array of account metadata. */ private async getUlnAccounts( connection: Connection, payer: PublicKey, ulnInfo: KeyedAccountInfo, ulnDefaultSendConfigInfo: KeyedAccountInfo, ulnSendConfigInfo: KeyedAccountInfo, quoteOrSend: QuoteOrSend, commitment?: Commitment | GetAccountInfoConfig ): Promise { invariant(ulnInfo.accountInfo && ulnDefaultSendConfigInfo.accountInfo, 'uln send library not initialized') const [ulnState] = UlnProgram.accounts.UlnSettings.fromAccountInfo(ulnInfo.accountInfo, 0) const [defaultSendConfigState] = UlnProgram.accounts.SendConfig.fromAccountInfo( ulnDefaultSendConfigInfo.accountInfo, 0 ) const sendConfigState = ulnSendConfigInfo.accountInfo ? UlnProgram.accounts.SendConfig.fromAccountInfo(ulnSendConfigInfo.accountInfo)[0] : null let { executor, uln: { requiredDvns, optionalDvns }, } = defaultSendConfigState // if the executor is set in the send config, use it if (sendConfigState?.executor && !sendConfigState.executor.executor.equals(PublicKey.default)) { ;({ executor } = sendConfigState) } // if the requiredDvns is set in the send config, use it if (sendConfigState && sendConfigState.uln.requiredDvns.length > 0) { requiredDvns = sendConfigState.uln.requiredDvns.filter((p) => { return !p.equals(PublicKey.default) }) } // if the optionalDvns is set in the send config, use it if (sendConfigState && sendConfigState.uln.optionalDvns.length > 0) { optionalDvns = sendConfigState.uln.optionalDvns.filter((p) => { return !p.equals(PublicKey.default) }) } const dvnsKey = requiredDvns.concat(optionalDvns) const [executorBuf, ...dvnBuf] = await this.getMultipleAccountsInfo( connection, [executor.executor, ...dvnsKey], commitment ) invariant(executorBuf, `executor:${executor.executor.toBase58()} not initialized`) let executorAccounts: AccountMeta[], dvnAccounts: AccountMeta[] { const executor = { config: ExecutorAccounts.ExecutorConfig.fromAccountInfo(executorBuf)[0], owner: executorBuf.owner, } const dvns = dvnBuf.map((dvn, i) => { invariant(dvn, `dvn:${dvnsKey[i].toBase58()} not initialized`) return { config: DvnAccounts.DvnConfig.fromAccountInfo(dvn)[0], owner: dvn.owner, } }) const priceFeeds = new Array() ;[executor.config, ...dvns.map((dvn) => dvn.config)].forEach((config) => { priceFeeds.push(config.priceFeed) }) const priceFeedInfos = await this.getMultipleAccountsInfo(connection, priceFeeds, commitment) priceFeedInfos.forEach((info, i) => { invariant(info, `priceFeed:${priceFeeds[i].toBase58()} not initialized`) }) executorAccounts = new Executor(executor.owner).getQuoteIXAccountMetaForCPI( executor.config.priceFeed, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (await this.getMultipleAccountsInfo(connection, [executor.config.priceFeed], commitment))[0]!.owner, quoteOrSend === QuoteOrSend.Send ) dvnAccounts = ( await Promise.all( dvns.map(async (p, i) => { return new DVN(p.owner).getQuoteIXAccountMetaForCPI( p.config.priceFeed, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (await this.getMultipleAccountsInfo(connection, [p.config.priceFeed], commitment))[0]! .owner, quoteOrSend === QuoteOrSend.Send ) }) ) ).flat() } const treasury = ulnState.treasury?.nativeReceiver let accounts: AccountMeta[] if (quoteOrSend === QuoteOrSend.Quote) { accounts = UlnProgram.instructions.createQuoteInstructionAccounts( { endpoint: PublicKey.default, // useless uln: this.uln.deriver.setting()[0], sendConfig: ulnSendConfigInfo.accountId, defaultSendConfig: ulnDefaultSendConfigInfo.accountId, anchorRemainingAccounts: executorAccounts.concat(dvnAccounts), } satisfies UlnProgram.instructions.QuoteInstructionAccounts, this.ulnProgram ) } else { accounts = UlnProgram.instructions.createSendInstructionAccounts( { endpoint: PublicKey.default, // useless uln: this.uln.deriver.setting()[0], sendConfig: ulnSendConfigInfo.accountId, defaultSendConfig: ulnDefaultSendConfigInfo.accountId, payer, treasury, eventAuthority: this.uln.eventAuthorityPDA, program: this.uln.program, anchorRemainingAccounts: executorAccounts.concat(dvnAccounts), } satisfies UlnProgram.instructions.SendInstructionAccounts, this.ulnProgram ) if (treasury) { const treasuryIndex = accounts.findIndex((k) => k.pubkey.toBase58() == treasury.toBase58()) accounts[treasuryIndex].isWritable = true // treasury account } } accounts.forEach((key) => { if (!payer.equals(key.pubkey)) { key.isSigner = false } }) return accounts.slice(1) } }