import { AccountMeta, Commitment, Connection, GetAccountInfoConfig, PublicKey, TransactionInstruction, } from '@solana/web3.js' import { EndpointId } from '@layerzerolabs/lz-definitions' import { HashSigner, keccak_256 } from '@layerzerolabs/lz-foundation' import { DVNDeriver, EventPDADeriver, PriceFeedPDADeriver } from './pda-deriver' import { InitDvnParams, MultisigConfig, multisigConfigBeet } from '../generated/solita/dvn' import * as accounts from '../generated/solita/dvn/accounts' import * as errors from '../generated/solita/dvn/errors' import * as events from '../generated/solita/dvn/events' import * as instructions from '../generated/solita/dvn/instructions' import * as types from '../generated/solita/dvn/types' export { accounts, errors, instructions, types, events } export { PROGRAM_ID } from '../generated/solita/dvn' /** * Interface representing a sign function. */ export interface SignFunc { /** * Signs a message. * * @param {Buffer} message - The message to sign. * @returns {Promise<{ signature: Uint8Array; recoveryId: number }[]>} A promise that resolves to an array of signature objects. */ sign(message: Buffer): Promise< { signature: Uint8Array recoveryId: number }[] > } /** * Class representing a Solana signer extension. */ export class SolanaSignerExt implements SignFunc { private readonly signers: HashSigner[] private constructor(signers: HashSigner[]) { this.signers = signers } /** * Creates an instance of the SolanaSignerExt class. * * @param {HashSigner[]} signers - The signers. * @returns {SolanaSignerExt} The SolanaSignerExt instance. */ static from(signers: HashSigner[]): SolanaSignerExt { return new SolanaSignerExt(signers) } /** * Signs a message. * * @param {Buffer} message - The message to sign. * @returns {Promise<{ signature: Uint8Array; recoveryId: number }[]>} A promise that resolves to an array of signature objects. */ async sign(message: Buffer): Promise<{ signature: Uint8Array; recoveryId: number }[]> { return Promise.all( this.signers.map(async (s) => { const rv = await s.signHash(message) return { signature: rv.slice(0, 64), recoveryId: rv[64] } }) ) } } /** * Class representing a DVN (Decentralized Validator Network). */ export class DVN { dvnDeriver: DVNDeriver vid: number eventAuthority: PublicKey /** * Creates an instance of the DVN class. * * @param {PublicKey} programId - The program ID. * @param {EndpointId} [endpointId=EndpointId.SOLANA_V2_SANDBOX] - The endpoint ID. */ constructor( readonly programId: PublicKey, endpointId: EndpointId = EndpointId.SOLANA_V2_SANDBOX ) { this.dvnDeriver = new DVNDeriver(programId) this.vid = endpointId % 30000 this.eventAuthority = new EventPDADeriver(programId).eventAuthority()[0] } /** * Initializes the DVN. * * @param {Connection} connection - The connection. * @param {PublicKey} payer - The payer. * @param {InitDvnParams} params - The initialization parameters. * @returns {Promise} A promise that resolves to the transaction instruction. * @throws {Error} If the DVN is already initialized. */ async initDVN(connection: Connection, payer: PublicKey, params: InitDvnParams): Promise { const [config] = this.dvnDeriver.config() const info = await connection.getAccountInfo(config) if (info) { throw new Error('DVN already initialized') } return instructions.createInitDvnInstruction( { payer, config, } satisfies instructions.InitDvnInstructionAccounts, { params, } satisfies instructions.InitDvnInstructionArgs, this.programId ) } /** * Gets the digest for a transaction. * * @param {number} vid - The validator ID. * @param {TransactionInstruction} instruction - The transaction instruction. * @param {number} expiration - The expiration time. * @returns {types.ExecuteTransactionDigest} The transaction digest. */ getDigest(vid: number, instruction: TransactionInstruction, expiration: number): types.ExecuteTransactionDigest { return { vid, programId: instruction.programId, accounts: instruction.keys.map((key) => { return { pubkey: key.pubkey, isSigner: key.isSigner, isWritable: key.isWritable, } }), data: instruction.data, expiration, } } /** * Gets the execute hash for a given hash bytes. * * @param {Buffer} hashBytes - The hash bytes. * @returns {PublicKey} The execute hash. */ getExecuteHash(hashBytes: Buffer): PublicKey { const [executeHash] = this.dvnDeriver.executeHash(hashBytes) return executeHash } /** * Gets the hash bytes for a given digest. * * @param {types.ExecuteTransactionDigest} digest - The transaction digest. * @returns {Buffer} The hash bytes. */ getHashBytes(digest: types.ExecuteTransactionDigest): Buffer { const [digestBytes] = types.executeTransactionDigestBeet.serialize(digest) const hash = keccak_256(digestBytes) return Buffer.from(hash) } /** * Invokes a transaction. * * @param {Connection} connection - The connection. * @param {number} vid - The validator ID. * @param {PublicKey} payer - The payer. * @param {TransactionInstruction} instruction - The transaction instruction. * @param {number} expiration - The expiration time. * @param {SignFunc} sign - The sign function. * @returns {Promise} A promise that resolves to the transaction instruction. * @throws {Error} If the DVN is not initialized. */ async invoke( connection: Connection, vid: number, payer: PublicKey, instruction: TransactionInstruction, expiration: number, sign: SignFunc ): Promise { const configState = await this.getConfigState(connection, 'confirmed') if (!configState) { throw new Error('DVN not initialized') } const [config] = this.dvnDeriver.config() const digest: types.ExecuteTransactionDigest = this.getDigest(vid, instruction, expiration) const hashBytes = this.getHashBytes(digest) const executeHash = this.getExecuteHash(hashBytes) const signatures: number[][] = [] const signResult = await sign.sign(hashBytes) signatures.push(...signResult.map((s) => Array.from(s.signature).concat([s.recoveryId]))) const param: types.InvokeParams = { digest, signatures, } const remainingAccounts = instruction.keys.map((key) => { key.isSigner = false return key }) return instructions.createInvokeInstruction( { signer: payer, config, executeHash, program: this.programId, eventAuthority: this.eventAuthority, anchorRemainingAccounts: [ { pubkey: instruction.programId, isSigner: false, isWritable: false, }, ].concat(remainingAccounts), } satisfies instructions.InvokeInstructionAccounts, { params: param, } satisfies instructions.InvokeInstructionArgs, this.programId ) } /** * Creates a transaction instruction to set the quorum. * * @param {number} quorum - The quorum. * @returns {TransactionInstruction} The transaction instruction. */ createSetQuorumInstruction(quorum: number): TransactionInstruction { const params: MultisigConfig = { fields: [quorum], __kind: 'Quorum', } const fixedBeet = multisigConfigBeet.toFixedFromValue(params) const buffer = Buffer.alloc(fixedBeet.byteSize) fixedBeet.write(buffer, 0, params) return new TransactionInstruction({ programId: this.programId, keys: [], data: buffer, }) } /** * Creates a transaction instruction to set the admins. * * @param {PublicKey[]} admins - The admin public keys. * @returns {TransactionInstruction} The transaction instruction. */ createSetAdminsInstruction(admins: PublicKey[]): TransactionInstruction { const params: MultisigConfig = { fields: [admins] satisfies [PublicKey[]], __kind: 'Admins', } const fixedBeet = multisigConfigBeet.toFixedFromValue(params) const buffer = Buffer.alloc(fixedBeet.byteSize) fixedBeet.write(buffer, 0, params) return new TransactionInstruction({ programId: this.programId, keys: [], data: buffer, }) } /** * Creates a transaction instruction to set the signers. * * @param {Uint8Array[]} signers - The signer public keys. * @returns {TransactionInstruction} The transaction instruction. */ createSetSignersInstruction(signers: Uint8Array[]): TransactionInstruction { const params: MultisigConfig = { fields: [signers.map((s) => Array.from(s))], __kind: 'Signers', } const fixedBeet = multisigConfigBeet.toFixedFromValue(params) const buffer = Buffer.alloc(fixedBeet.byteSize) fixedBeet.write(buffer, 0, params) return new TransactionInstruction({ programId: this.programId, keys: [], data: buffer, }) } /** * Creates a transaction instruction to set the allowlist. * * @param {PublicKey[]} allowlist - The allowlist public keys. * @returns {TransactionInstruction} The transaction instruction. */ createSetAllowlistInstruction(allowlist: PublicKey[]): TransactionInstruction { const params: MultisigConfig = { fields: [allowlist], __kind: 'Allowlist', } const fixedBeet = multisigConfigBeet.toFixedFromValue(params) const buffer = Buffer.alloc(fixedBeet.byteSize) fixedBeet.write(buffer, 0, params) return new TransactionInstruction({ programId: this.programId, keys: [], data: buffer, }) } /** * Creates a transaction instruction to set the denylist. * * @param {PublicKey[]} denylist - The denylist public keys. * @returns {TransactionInstruction} The transaction instruction. */ createSetDenylistInstruction(denylist: PublicKey[]): TransactionInstruction { const params: MultisigConfig = { fields: [denylist], __kind: 'Denylist', } const fixedBeet = multisigConfigBeet.toFixedFromValue(params) const buffer = Buffer.alloc(fixedBeet.byteSize) fixedBeet.write(buffer, 0, params) return new TransactionInstruction({ programId: this.programId, keys: [], data: buffer, }) } /** * Creates a transaction instruction to set the pause state. * * @param {boolean} pause - The pause state. * @returns {TransactionInstruction} The transaction instruction. */ createSetPauseInstruction(pause: boolean): TransactionInstruction { const params: MultisigConfig = { fields: [pause], __kind: 'Paused', } const fixedBeet = multisigConfigBeet.toFixedFromValue(params) const buffer = Buffer.alloc(fixedBeet.byteSize) fixedBeet.write(buffer, 0, params) return new TransactionInstruction({ programId: this.programId, keys: [], data: buffer, }) } /** * Creates a transaction instruction to set the default multiplier basis points. * * @param {PublicKey} admin - The admin public key. * @param {number} defaultMultiplierBps - The default multiplier basis points. * @returns {TransactionInstruction} The transaction instruction. */ createSetDefaultMultiplierBpsInstruction(admin: PublicKey, defaultMultiplierBps: number): TransactionInstruction { const [configAccount] = this.dvnDeriver.config() return instructions.createSetConfigInstruction( { admin, config: configAccount, program: this.programId, eventAuthority: this.eventAuthority, } satisfies instructions.SetConfigInstructionAccounts, { params: { config: { fields: [defaultMultiplierBps], __kind: 'DefaultMultiplierBps', }, }, } satisfies instructions.SetConfigInstructionArgs, this.programId ) } /** * Creates a transaction instruction to change the admins. * * @param {PublicKey} admin - The admin public key. * @param {PublicKey[]} admins - The new admin public keys. * @returns {TransactionInstruction} The transaction instruction. */ createChangeAdminsInstruction(admin: PublicKey, admins: PublicKey[]): TransactionInstruction { const [configAccount] = this.dvnDeriver.config() return instructions.createSetConfigInstruction( { admin, config: configAccount, program: this.programId, eventAuthority: this.eventAuthority, } satisfies instructions.SetConfigInstructionAccounts, { params: { config: { fields: [admins], __kind: 'Admins', }, }, } satisfies instructions.SetConfigInstructionArgs, this.programId ) } /** * Creates a transaction instruction to set the destination configurations. * * @param {PublicKey} admin - The admin public key. * @param {types.DstConfig[]} dstConfigs - The destination configurations. * @returns {TransactionInstruction} The transaction instruction. */ createSetDstConfigInstruction(admin: PublicKey, dstConfigs: types.DstConfig[]): TransactionInstruction { const [configAccount] = this.dvnDeriver.config() return instructions.createSetConfigInstruction( { admin, config: configAccount, program: this.programId, eventAuthority: this.eventAuthority, } satisfies instructions.SetConfigInstructionAccounts, { params: { config: { fields: [dstConfigs], __kind: 'DstConfigs', }, }, } satisfies instructions.SetConfigInstructionArgs, this.programId ) } /** * Creates a transaction instruction to remove the destination configurations. * * @param {PublicKey} admin - The admin public key. * @param {number[]} removeEids - The endpoint IDs to remove. * @returns {TransactionInstruction} The transaction instruction. */ createRemoveDstConfigInstruction(admin: PublicKey, removeEids: number[]): TransactionInstruction { const [configAccount] = this.dvnDeriver.config() return instructions.createSetConfigInstruction( { admin, config: configAccount, program: this.programId, eventAuthority: this.eventAuthority, } satisfies instructions.SetConfigInstructionAccounts, { params: { config: { fields: [removeEids], __kind: 'RemoveDstConfigs', }, }, } satisfies instructions.SetConfigInstructionArgs, this.programId ) } /** * Creates a transaction instruction to set the message libraries. * * @param {PublicKey[]} msglibPrograms - The message library public keys. * @returns {TransactionInstruction} The transaction instruction. */ createSetMsgLibsInstruction(msglibPrograms: PublicKey[]): TransactionInstruction { const params: MultisigConfig = { fields: [msglibPrograms], __kind: 'Msglibs', } const fixedBeet = multisigConfigBeet.toFixedFromValue(params) const buffer = Buffer.alloc(fixedBeet.byteSize) fixedBeet.write(buffer, 0, params) return new TransactionInstruction({ programId: this.programId, keys: [], data: buffer, }) } /** * Gets the expiration time. * * @returns {number} The expiration time. */ _getExpiration(): number { return new Date().getTime() / 1000 + 120 } /** * Sets the message libraries. * * @param {Connection} connection - The connection. * @param {PublicKey} payer - The payer. * @param {PublicKey[]} msgLibs - The message library public keys. * @param {SignFunc} sign - The sign function. * @returns {Promise} A promise that resolves to the transaction instruction. */ async setMsgLibs( connection: Connection, payer: PublicKey, msgLibs: PublicKey[], sign: SignFunc ): Promise { const expiration = this._getExpiration() return this.invoke(connection, this.vid, payer, this.createSetMsgLibsInstruction(msgLibs), expiration, sign) } /** * Creates a transaction instruction to set the price feed. * * @param {PublicKey} admin - The admin public key. * @param {PublicKey} priceFeedProgram - The price feed program public key. * @returns {TransactionInstruction} The transaction instruction. */ createSetPriceFeedInstruction(admin: PublicKey, priceFeedProgram: PublicKey): TransactionInstruction { const [configAccount] = this.dvnDeriver.config() const [priceFeedPda] = new PriceFeedPDADeriver(priceFeedProgram).priceFeed() return instructions.createSetConfigInstruction( { admin, config: configAccount, program: this.programId, eventAuthority: this.eventAuthority, } satisfies instructions.SetConfigInstructionAccounts, { params: { config: { fields: [priceFeedPda], __kind: 'PriceFeed', }, }, } satisfies instructions.SetConfigInstructionArgs, this.programId ) } /** * Gets the account metadata for the CPI (Cross-Program Invocation) of the quote instruction. * * @param {PublicKey} priceFeedConfig - The price feed configuration public key. * @param {PublicKey} priceFeedProgram - The price feed program public key. * @param {boolean} payment - Indicates if payment is required. * @returns {AccountMeta[]} An array of account metadata. */ getQuoteIXAccountMetaForCPI( priceFeedConfig: PublicKey, priceFeedProgram: PublicKey, payment: boolean ): AccountMeta[] { const [config] = this.dvnDeriver.config() const keys = instructions.createQuoteDvnInstructionAccounts( { dvnConfig: config, priceFeedConfig, priceFeedProgram: priceFeedProgram, } satisfies instructions.QuoteDvnInstructionAccounts, this.programId ) if (payment) { // if payment is required, the first account is required to be writable to receive the payment keys[0].isWritable = true } return [ { pubkey: this.programId, isWritable: false, isSigner: false, }, ].concat(keys) } /** * Gets the configuration state. * * @param {Connection} connection - The connection to the Solana cluster. * @param {Commitment | GetAccountInfoConfig} [commitment='confirmed'] - The commitment level or account info configuration. * @returns {Promise} A promise that resolves to the configuration state or null if not found. */ async getConfigState( connection: Connection, commitment: Commitment | GetAccountInfoConfig = 'confirmed' ): Promise { const [config] = this.dvnDeriver.config() try { return await accounts.DvnConfig.fromAccountAddress(connection, config, commitment) } catch (e) { return null } } }