import {
address,
createKeyPairSignerFromBytes,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
getAddressEncoder,
getProgramDerivedAddress,
getSignatureFromTransaction,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import crypto from "crypto";
import dotenv from "dotenv";
dotenv.config();
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY);
const solanaKeypair = await createKeyPairSignerFromBytes(
Uint8Array.from(solanaPrivateKey),
);
// Solana CCTP Program Addresses (Devnet)
const MESSAGE_TRANSMITTER_PROGRAM = address(
"CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC",
);
const TOKEN_MESSENGER_MINTER_PROGRAM = address(
"CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe",
);
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const ASSOCIATED_TOKEN_PROGRAM = address(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);
async function retryMintOnSolana(attestationData, sourceDomain) {
console.log("Retrying mint on Solana...");
const addressEncoder = getAddressEncoder();
// Derive receiver's USDC token account
const [receiverUsdcAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [
addressEncoder.encode(solanaKeypair.address),
addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
addressEncoder.encode(USDC_MINT),
],
});
// Derive required PDAs
const [messageTransmitter] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter")],
});
const [authorityPda] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter_authority")],
});
// Calculate used nonces PDA
const messageBytes = Buffer.from(attestationData.message.slice(2), "hex");
const nonce = messageBytes.readBigUInt64BE(12);
const firstNonce = (nonce / 6400n) * 6400n;
const firstNonceBuffer = Buffer.alloc(8);
firstNonceBuffer.writeBigUInt64BE(firstNonce);
const sourceDomainBuffer = Buffer.alloc(4);
sourceDomainBuffer.writeUInt32BE(sourceDomain);
const [usedNonces] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [
new TextEncoder().encode("used_nonces"),
sourceDomainBuffer,
firstNonceBuffer,
],
});
// Derive TokenMessengerMinterV2 PDAs
const [tokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_messenger")],
});
const [remoteTokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("remote_token_messenger"),
new TextEncoder().encode(sourceDomain.toString()),
],
});
const [tokenMinter] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_minter")],
});
const [localToken] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("local_token"),
addressEncoder.encode(USDC_MINT),
],
});
const sourceTokenBytes = messageBytes.slice(133, 165);
const [tokenPair] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("token_pair"),
sourceDomainBuffer,
sourceTokenBytes,
],
});
const [custody] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("custody"),
addressEncoder.encode(USDC_MINT),
],
});
const [eventAuthority] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const [tokenProgramEventAuthority] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
// Build instruction
const discriminator = crypto
.createHash("sha256")
.update("global:receive_message")
.digest()
.slice(0, 8);
const messageBuffer = Buffer.from(attestationData.message.slice(2), "hex");
const attestationBuffer = Buffer.from(
attestationData.attestation.slice(2),
"hex",
);
const messageLenBuffer = Buffer.alloc(4);
messageLenBuffer.writeUInt32LE(messageBuffer.length);
const attestationLenBuffer = Buffer.alloc(4);
attestationLenBuffer.writeUInt32LE(attestationBuffer.length);
const instructionData = new Uint8Array(
Buffer.concat([
discriminator,
messageLenBuffer,
messageBuffer,
attestationLenBuffer,
attestationBuffer,
]),
);
const receiveMessageIx = {
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
accounts: [
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: solanaKeypair.address, role: 0 },
{ address: authorityPda, role: 0 },
{ address: messageTransmitter, role: 0 },
{ address: usedNonces, role: 1 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: SYSTEM_PROGRAM_ADDRESS, role: 0 },
{ address: eventAuthority, role: 0 },
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
{ address: tokenMessenger, role: 0 },
{ address: remoteTokenMessenger, role: 0 },
{ address: tokenMinter, role: 1 },
{ address: localToken, role: 1 },
{ address: tokenPair, role: 0 },
{ address: receiverUsdcAccount, role: 1 },
{ address: custody, role: 1 },
{ address: TOKEN_PROGRAM_ADDRESS, role: 0 },
{ address: tokenProgramEventAuthority, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
],
data: instructionData,
};
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstruction(receiveMessageIx, tx),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
try {
await sendAndConfirmTransaction(signedTransaction, {
commitment: "confirmed",
});
const signature = getSignatureFromTransaction(signedTransaction);
console.log(`Mint successful! Signature: ${signature}`);
return signature;
} catch (error) {
if (error.message.includes("already been processed")) {
console.log("Nonce already used - mint may have already completed.");
}
throw error;
}
}
// Use attestation data from the API
const attestationData = {
message: "0x000000000000000500000000...",
attestation: "0xdc485fb2f9a8f68c871f4ca7386dee9086ff9d43...",
};
await retryMintOnSolana(attestationData, 0);