@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
364 lines • 17.6 kB
JavaScript
import { addressToBytes, assert, padBytesToLength } from '@hyperlane-xyz/utils';
import { ComputeBudgetProgram, Keypair, Message, PublicKey, SystemProgram, Transaction, TransactionInstruction, VersionedTransaction, } from '@solana/web3.js';
import { serialize } from 'borsh';
import { SEALEVEL_SPL_NOOP_ADDRESS } from '../../consts/sealevel.js';
import { SealevelOverheadIgpAdapter, } from '../../gas/adapters/SealevelIgpAdapter.js';
import { SealevelInstructionWrapper } from '../../utils/sealevelSerialization.js';
import { SealevelHypCollateralAdapter, TRANSFER_REMOTE_COMPUTE_LIMIT, } from './SealevelTokenAdapter.js';
import { SealevelCCHandleLocalInstruction, SealevelCCHandleLocalSchema, SealevelCCInstructionKind, SealevelCCTransferRemoteToInstruction, SealevelCCTransferRemoteToSchema, encodeTokenMessage, } from './serialization.js';
// CC program discriminator (8 bytes of 2s)
const CC_DISCRIMINATOR = Buffer.from([2, 2, 2, 2, 2, 2, 2, 2]);
// Each SerializableAccountMeta is: pubkey (32) + is_signer (1) + is_writable (1) = 34 bytes
const SERIALIZABLE_ACCOUNT_META_SIZE = 34;
export class SealevelHypCrossCollateralAdapter extends SealevelHypCollateralAdapter {
deriveCrossCollateralStatePda() {
return this.derivePda(['hyperlane_token', '-', 'cross_collateral'], this.warpProgramPubKey);
}
deriveCrossCollateralDispatchAuthorityPda() {
return this.derivePda(['hyperlane_cc', '-', 'dispatch_authority'], this.warpProgramPubKey);
}
async quoteTransferRemoteToGas(params) {
const localDomain = this.multiProvider.getDomainId(this.chainName);
if (params.destination === localDomain) {
return { igpQuote: { amount: 0n } };
}
const quote = await this.quoteTransferRemoteGas({
destination: params.destination,
sender: params.sender,
});
return {
igpQuote: { amount: BigInt(quote.igpQuote.amount) },
};
}
// Should match rust/sealevel/programs/hyperlane-sealevel-token-cross-collateral/src/processor.rs transfer_remote_to_remote
//
// 0. [executable] The system program.
// 1. [] The token PDA account.
// 2. [] The cross-collateral state PDA account.
// 3. [executable] The spl_noop program.
// 4. [executable] The mailbox program.
// 5. [writeable] The mailbox outbox account.
// 6. [] Message dispatch authority.
// 7. [signer] The token sender and mailbox payer.
// 8. [signer] Unique message account.
// 9. [writeable] Message storage PDA.
// 10+. (optional) IGP accounts.
// N. [executable] The SPL token program for the mint.
// N+1. [writeable] The mint.
// N+2. [writeable] The token sender's associated token account.
// N+3. [writeable] The escrow PDA account.
async getTransferRemoteToRemoteKeyList({ sender, mailbox, randomWallet, igp, }) {
let keys = [
// 0. [executable] The system program.
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
// 1. [] The token PDA account.
{
pubkey: this.deriveHypTokenAccount(),
isSigner: false,
isWritable: false,
},
// 2. [] The cross-collateral state PDA account.
{
pubkey: this.deriveCrossCollateralStatePda(),
isSigner: false,
isWritable: false,
},
// 3. [executable] The spl_noop program.
{
pubkey: new PublicKey(SEALEVEL_SPL_NOOP_ADDRESS),
isSigner: false,
isWritable: false,
},
// 4. [executable] The mailbox program.
{ pubkey: mailbox, isSigner: false, isWritable: false },
// 5. [writeable] The mailbox outbox account.
{
pubkey: this.deriveMailboxOutboxAccount(mailbox),
isSigner: false,
isWritable: true,
},
// 6. [] Message dispatch authority.
{
pubkey: this.deriveMessageDispatchAuthorityAccount(),
isSigner: false,
isWritable: false,
},
// 7. [signer] The token sender and mailbox payer.
{ pubkey: sender, isSigner: true, isWritable: false },
// 8. [signer] Unique message account.
{ pubkey: randomWallet, isSigner: true, isWritable: false },
// 9. [writeable] Message storage PDA.
{
pubkey: this.deriveMsgStorageAccount(mailbox, randomWallet),
isSigner: false,
isWritable: true,
},
];
if (igp) {
keys = [
...keys,
// 10. [executable] The IGP program.
{ pubkey: igp.programId, isSigner: false, isWritable: false },
// 11. [writeable] The IGP program data.
{
pubkey: SealevelOverheadIgpAdapter.deriveIgpProgramPda(igp.programId),
isSigner: false,
isWritable: true,
},
// 12. [writeable] Gas payment PDA.
{
pubkey: SealevelOverheadIgpAdapter.deriveGasPaymentPda(igp.programId, randomWallet),
isSigner: false,
isWritable: true,
},
];
if (igp.overheadIgpAccount) {
keys = [
...keys,
// 13. [] OPTIONAL - The Overhead IGP account, if the configured IGP is an Overhead IGP.
{
pubkey: igp.overheadIgpAccount,
isSigner: false,
isWritable: false,
},
];
}
keys = [
...keys,
// 14. [writeable] The Overhead's inner IGP account (or the normal IGP account if there's no Overhead IGP).
{ pubkey: igp.igpAccount, isSigner: false, isWritable: true },
];
}
keys = [
...keys,
// N. [executable] The SPL token program for the mint.
{
pubkey: await this.getTokenProgramId(),
isSigner: false,
isWritable: false,
},
// N+1. [writeable] The mint.
{ pubkey: this.tokenMintPubKey, isSigner: false, isWritable: true },
// N+2. [writeable] The token sender's associated token account.
{
pubkey: await this.deriveAssociatedTokenAccount(sender),
isSigner: false,
isWritable: true,
},
// N+3. [writeable] The escrow PDA account.
{ pubkey: this.deriveEscrowAccount(), isSigner: false, isWritable: true },
];
return keys;
}
// Simulates the HandleLocalAccountMetas instruction on the target program
// to discover accounts needed for the HandleLocal CPI call.
// Same simulation pattern as SealevelIgpAdapter.quoteGasPayment.
//
// Should match handle_local_account_metas in processor.rs:
// Account 0: [] The target program's account metas PDA.
async simulateHandleLocalAccountMetas({ targetProgram, senderProgram, amount, recipient, payer, }) {
const tokenMessage = encodeTokenMessage(recipient, amount);
const value = new SealevelInstructionWrapper({
instruction: SealevelCCInstructionKind.HandleLocalAccountMetas,
data: new SealevelCCHandleLocalInstruction({
sender_program_id: senderProgram.toBytes(),
message: tokenMessage,
}),
});
const serializedData = serialize(SealevelCCHandleLocalSchema, value);
// Derive the target program's account metas PDA
const targetTokenPda = this.derivePda(['hyperlane_message_recipient', '-', 'handle', '-', 'account_metas'], targetProgram);
const instruction = new TransactionInstruction({
keys: [
// Account 0: The target program's account metas PDA.
{ pubkey: targetTokenPda, isSigner: false, isWritable: false },
],
programId: targetProgram,
data: Buffer.concat([CC_DISCRIMINATOR, Buffer.from(serializedData)]),
});
const message = Message.compile({
recentBlockhash: PublicKey.default.toBase58(),
instructions: [instruction],
payerKey: payer,
});
const tx = new VersionedTransaction(message);
const connection = this.getProvider();
const simulationResponse = await connection.simulateTransaction(tx, {
replaceRecentBlockhash: true,
sigVerify: false,
});
const { returnData, err, logs } = simulationResponse.value;
assert(!err, `HandleLocalAccountMetas simulation failed: ${JSON.stringify(err)}\nLogs: ${logs?.join('\n')}`);
const base64Data = returnData?.data?.[0];
assert(base64Data, `HandleLocalAccountMetas simulation returned no data. The target program may not implement HandleLocalAccountMetas.\nLogs: ${logs?.join('\n')}`);
const data = Buffer.from(base64Data, 'base64');
// First 4 bytes are the Vec length (little-endian u32)
const count = data.readUInt32LE(0);
const expectedLength = 4 + count * SERIALIZABLE_ACCOUNT_META_SIZE;
assert(data.length >= expectedLength, `HandleLocalAccountMetas returned truncated data: expected ${expectedLength} bytes, got ${data.length}`);
const accountMetas = [];
for (let i = 0; i < count; i++) {
const offset = 4 + i * SERIALIZABLE_ACCOUNT_META_SIZE;
const pubkey = new PublicKey(data.subarray(offset, offset + 32));
const isSigner = data[offset + 32] !== 0;
const isWritable = data[offset + 33] !== 0;
accountMetas.push({ pubkey, isSigner, isWritable });
}
return accountMetas;
}
// Should match rust/sealevel/programs/hyperlane-sealevel-token-cross-collateral/src/processor.rs transfer_remote_to_local
//
// 0. [executable] The system program.
// 1. [] The token PDA account.
// 2. [] The cross-collateral state PDA account.
// 3. [signer] The token sender and payer.
// 4. [] The cross-collateral dispatch authority PDA.
// 5. [executable] The target program.
// 6. [executable] The SPL token program for the mint.
// 7. [writeable] The mint.
// 8. [writeable] The token sender's associated token account.
// 9. [writeable] The escrow PDA account.
// 10+. (variable) Target HandleLocal accounts (from simulation).
async getTransferRemoteToLocalKeyList({ sender, targetProgram, senderProgram, amount, recipient, }) {
const handleLocalAccountMetas = await this.simulateHandleLocalAccountMetas({
targetProgram,
senderProgram,
amount,
recipient,
payer: sender,
});
assert(handleLocalAccountMetas[0]?.pubkey.equals(this.deriveCrossCollateralDispatchAuthorityPda()), 'Expected first HandleLocal account to be the CC dispatch authority PDA');
const keys = [
// 0. [executable] The system program.
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
// 1. [] The token PDA account.
{
pubkey: this.deriveHypTokenAccount(),
isSigner: false,
isWritable: false,
},
// 2. [] The cross-collateral state PDA account.
{
pubkey: this.deriveCrossCollateralStatePda(),
isSigner: false,
isWritable: false,
},
// 3. [signer] The token sender and payer.
{ pubkey: sender, isSigner: true, isWritable: true },
// 4. [] The cross-collateral dispatch authority PDA.
{
pubkey: this.deriveCrossCollateralDispatchAuthorityPda(),
isSigner: false,
isWritable: false,
},
// 5. [executable] The target program.
{ pubkey: targetProgram, isSigner: false, isWritable: false },
// 6. [executable] The SPL token program for the mint.
{
pubkey: await this.getTokenProgramId(),
isSigner: false,
isWritable: false,
},
// 7. [writeable] The mint.
{ pubkey: this.tokenMintPubKey, isSigner: false, isWritable: true },
// 8. [writeable] The token sender's associated token account.
{
pubkey: await this.deriveAssociatedTokenAccount(sender),
isSigner: false,
isWritable: true,
},
// 9. [writeable] The escrow PDA account.
{ pubkey: this.deriveEscrowAccount(), isSigner: false, isWritable: true },
// 10+. Target HandleLocal accounts (from simulation).
// Skip index 0 (cc_dispatch_authority) — transfer_remote_to_local
// prepends it to the CPI, so remaining_accounts starts at index 1.
...handleLocalAccountMetas.slice(1),
];
return keys;
}
// For remote transfers, `recipient` is a destination-chain address (e.g., Ethereum hex).
// For local (same-chain) transfers, `recipient` must be a valid Solana pubkey
// that can receive the target token (used to derive the recipient's ATA).
async populateTransferRemoteToTx({ amount, destination, recipient, fromAccountOwner, targetRouter, extraSigners, }) {
assert(fromAccountOwner, 'fromAccountOwner required for Sealevel');
const sender = new PublicKey(fromAccountOwner);
const recipientBytes = padBytesToLength(addressToBytes(recipient), 32);
const targetRouterBytes = padBytesToLength(addressToBytes(targetRouter), 32);
const localDomain = this.multiProvider.getDomainId(this.chainName);
if (destination === localDomain) {
const targetProgram = new PublicKey(targetRouter);
const keys = await this.getTransferRemoteToLocalKeyList({
sender,
targetProgram,
senderProgram: this.warpProgramPubKey,
amount: BigInt(amount),
recipient: recipientBytes,
});
return this.createTransferRemoteToTx({
keys,
destination,
recipientBytes,
amount: BigInt(amount),
targetRouterBytes,
sender,
});
}
else {
const randomWallet = extraSigners?.length
? extraSigners[0]
: Keypair.generate();
const mailbox = new PublicKey(this.addresses.mailbox);
const keys = await this.getTransferRemoteToRemoteKeyList({
sender,
mailbox,
randomWallet: randomWallet.publicKey,
igp: await this.getIgpKeys(),
});
return this.createTransferRemoteToTx({
keys,
destination,
recipientBytes,
amount: BigInt(amount),
targetRouterBytes,
sender,
randomWallet,
});
}
}
async createTransferRemoteToTx({ keys, destination, recipientBytes, amount, targetRouterBytes, sender, randomWallet, }) {
const value = new SealevelInstructionWrapper({
instruction: SealevelCCInstructionKind.TransferRemoteTo,
data: new SealevelCCTransferRemoteToInstruction({
destination_domain: destination,
recipient: recipientBytes,
amount_or_id: amount,
target_router: targetRouterBytes,
}),
});
const serializedData = serialize(SealevelCCTransferRemoteToSchema, value);
const transferInstruction = new TransactionInstruction({
keys,
programId: this.warpProgramPubKey,
data: Buffer.concat([CC_DISCRIMINATOR, Buffer.from(serializedData)]),
});
const setComputeLimitInstruction = ComputeBudgetProgram.setComputeUnitLimit({ units: TRANSFER_REMOTE_COMPUTE_LIMIT });
const setPriorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: (await this.getMedianPriorityFee()) || 0,
});
const recentBlockhash = (await this.getProvider().getLatestBlockhash('finalized')).blockhash;
// @ts-expect-error Workaround for bug in the web3 lib, sometimes uses recentBlockhash and sometimes uses blockhash
const tx = new Transaction({
feePayer: sender,
blockhash: recentBlockhash,
recentBlockhash,
})
.add(setComputeLimitInstruction)
.add(setPriorityFeeInstruction)
.add(transferInstruction);
if (randomWallet) {
tx.partialSign(randomWallet);
}
return tx;
}
}
//# sourceMappingURL=SealevelCrossCollateralAdapter.js.map