xdapp-book/projects/messenger/handlers/solana.ts

351 lines
13 KiB
TypeScript

import * as fs from 'fs';
import { exec } from "child_process";
import { Solana as MessengerTypes} from '../chains/solana/target/types/solana';
import fetch from 'node-fetch';
import {
getEmitterAddressEth,
getEmitterAddressSolana,
parseSequenceFromLogSolana,
postVaaSolanaWithRetry,
getSignedVAAHash,
parseVaa,
} from '@certusone/wormhole-sdk';
import * as anchor from '@project-serum/anchor';
import { findProgramAddressSync } from '@project-serum/anchor/dist/cjs/utils/pubkey';
import * as byteify from 'byteify';
import keccak256 from 'keccak256';
const config = JSON.parse(fs.readFileSync('./xdapp.config.json').toString());
const IDL = JSON.parse(fs.readFileSync("./chains/solana/target/idl/solana.json").toString());
const CONTRACT_ADDRESS = "24FoTeX7BKbhTh3UF3feWusoAVKDPWZneiEqhXLVzZPL";
const KEYPAIR = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync('./chains/solana/keypairs/id.json').toString())));
export async function deploy(src: string){
const rpc = config.networks[src]['rpc'];
//Request Airdrop for saved keypair (because Local Validator probably started with the keypair in ~/.config)
const connection = new anchor.web3.Connection(rpc);
await connection.requestAirdrop(KEYPAIR.publicKey, 1e9*1000); //request 1000 SOL
await new Promise((r) => setTimeout(r, 15000)); // wait for the airdrop to go through
console.log("This will take a couple minutes due to Solana being solana...");
exec(
`cd chains/solana && solana config set -u ${rpc} -k keypairs/id.json && anchor build && anchor deploy --program-name solana --program-keypair keypairs/solana-keypair.json && exit`,
(err, out, errStr) => {
if(err){
throw new Error(err.message);
}
if(out) {
fs.writeFileSync(
`./deployinfo/${src}.deploy.json`,
JSON.stringify({
address: CONTRACT_ADDRESS,
vaas: []
}, null, 4)
);
new Promise((r) => setTimeout(r, 15000)) // wait for the chain to recognize the program
.then(async () => {
//Initalize the Contract
const messenger = new anchor.Program<MessengerTypes>(
IDL,
CONTRACT_ADDRESS,
new anchor.AnchorProvider(
new anchor.web3.Connection(rpc),
new anchor.Wallet(KEYPAIR),
{}));
const [configAcc, _] = findProgramAddressSync([
Buffer.from("config")
], messenger.programId);
await messenger.methods.initialize()
.accounts({
config: configAcc,
owner: messenger.provider.publicKey,
systemProgram: anchor.web3.SystemProgram.programId
})
.rpc();
})
}
}
)
}
export async function registerApp(src:string, target:string){
const srcNetwork = config.networks[src];
const targetNetwork = config.networks[target];
let srcDeploymentInfo;
let targetDeploymentInfo;
let targetEmitter;
try{
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
} catch (e){
throw new Error(`${src} is not deployed yet`);
}
try{
targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
} catch (e){
throw new Error(`${target} is not deployed yet`);
}
switch (targetNetwork['type']){
case 'evm':
targetEmitter = getEmitterAddressEth(targetDeploymentInfo['address']);
break;
case 'solana':
targetEmitter = await getEmitterAddressSolana(targetDeploymentInfo['address']);
break;
}
const messenger = new anchor.Program<MessengerTypes>(
IDL,
CONTRACT_ADDRESS,
new anchor.AnchorProvider(
new anchor.web3.Connection(srcNetwork['rpc']),
new anchor.Wallet(KEYPAIR),
{}));
const [emitterAcc, emitterBmp] = findProgramAddressSync([
Buffer.from("EmitterAddress"),
byteify.serializeUint16(targetNetwork.wormholeChainId)
], messenger.programId)
const [configAcc, configBmp] = findProgramAddressSync([
Buffer.from("config")
], messenger.programId);
const tx = await messenger.methods
.registerChain(
targetNetwork.wormholeChainId,
targetEmitter
)
.accounts({
owner: messenger.provider.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
config: configAcc,
emitterAcc: emitterAcc
})
.rpc();
return tx;
}
export async function sendMsg(src:string, msg:string){
const srcNetwork = config.networks[src];
let srcDeploymentInfo;
try{
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
} catch (e){
throw new Error(`${src} is not deployed yet`);
}
const messenger = new anchor.Program<MessengerTypes>(
IDL,
CONTRACT_ADDRESS,
new anchor.AnchorProvider(
new anchor.web3.Connection(srcNetwork['rpc']),
new anchor.Wallet(KEYPAIR),
{}
));
const whCoreBridge = new anchor.web3.PublicKey(srcNetwork.bridgeAddress);
const whConfig = findProgramAddressSync([Buffer.from("Bridge")], whCoreBridge)[0];
const whFeeCollector = findProgramAddressSync([Buffer.from("fee_collector")], whCoreBridge)[0];
//The emitter address for Solana contracts is *NOT* the contract address, but rather a PDA of the Contract
const whDerivedEmitter = findProgramAddressSync([Buffer.from("emitter")], messenger.programId)[0];
const whSequence = findProgramAddressSync([Buffer.from("Sequence"), whDerivedEmitter.toBytes()], whCoreBridge)[0];
//Throw away account used to store the message as logs on Solana are not archived for long term retreival
const whMessageKeypair = anchor.web3.Keypair.generate();
const [configAcc, configBmp] = findProgramAddressSync([
Buffer.from("config")
], messenger.programId);
const tx = await messenger.methods
.sendMsg(msg)
.accounts({
coreBridge: whCoreBridge,
wormholeConfig: whConfig,
wormholeFeeCollector: whFeeCollector,
wormholeDerivedEmitter: whDerivedEmitter,
wormholeSequence: whSequence,
wormholeMessageKey: whMessageKeypair.publicKey,
payer: messenger.provider.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
config: configAcc
})
.signers([
whMessageKeypair
])
.rpc();
const seq = parseSequenceFromLogSolana(await messenger.provider.connection.getTransaction(tx))
const emitterAddr = await getEmitterAddressSolana(messenger.programId.toString()); //same as whDerivedEmitter
await new Promise((r) => setTimeout(r, 5000)); // Wait for guardian to pick up message
console.log(
"Searching for: ",
`${config.wormhole.restAddress}/v1/signed_vaa/${srcNetwork.wormholeChainId}/${emitterAddr}/${seq}`
);
const vaaBytes = await (
await fetch(
`${config.wormhole.restAddress}/v1/signed_vaa/${srcNetwork.wormholeChainId}/${emitterAddr}/${seq}`
)
).json();
if(!vaaBytes['vaaBytes']){
throw new Error("VAA not found!");
}
if(!srcDeploymentInfo['vaas']){
srcDeploymentInfo['vaas'] = [vaaBytes['vaaBytes']]
} else {
srcDeploymentInfo['vaas'].push(vaaBytes['vaaBytes'])
}
fs.writeFileSync(
`./deployinfo/${src}.deploy.json`,
JSON.stringify(srcDeploymentInfo, null, 4)
);
return vaaBytes['vaaBytes'];
}
export async function submitVaa(src:string, target:string, idx:string){
const srcNetwork = config.networks[src];
let srcDeploymentInfo;
let targetDeploymentInfo;
try{
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
} catch (e){
throw new Error(`${src} is not deployed yet`);
}
try{
targetDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${target}.deploy.json`).toString());
} catch (e){
throw new Error(`${target} is not deployed yet`);
}
const vaa = isNaN(parseInt(idx))
? targetDeploymentInfo.vaas.pop()
: targetDeploymentInfo.vaas[parseInt(idx)];
const messenger = new anchor.Program<MessengerTypes>(
IDL,
CONTRACT_ADDRESS,
new anchor.AnchorProvider(
new anchor.web3.Connection(srcNetwork['rpc']),
new anchor.Wallet(KEYPAIR),
{}
));
//Submitting the VAA is a 2 step process
// First we submit it to core bridge to validate the signatures
await postVaaSolanaWithRetry(
messenger.provider.connection,
async (tx) => {
tx.partialSign(KEYPAIR);
return tx;
},
srcNetwork.bridgeAddress,
KEYPAIR.publicKey.toString(),
Buffer.from(vaa, "base64"),
10
);
// Then we submit to our program to go through the program's checks
await new Promise((r) => setTimeout(r, 5000));
const parsed_vaa = parseVaa(Buffer.from(vaa, 'base64'));
const vaaHash = getVaaHash(parsed_vaa); //await getSignedVAAHash(Buffer.from(vaa, "base64"));
//console.log("Hash: ", vaaHash, await getSignedVAAHash(Buffer.from(vaa, "base64")));
// Account that we stored the registered foreign emitter
let emitterAddressAcc = findProgramAddressSync([
Buffer.from("EmitterAddress"),
byteify.serializeUint16(parsed_vaa.emitterChain)
], messenger.programId)[0];
// A blank account we're creating just to keep track of already processed messages
let processedVaaKey = findProgramAddressSync([
Buffer.from(getEmitterAddressEth(targetDeploymentInfo.address), "hex"),
byteify.serializeUint16(parsed_vaa.emitterChain),
byteify.serializeUint64(Number(parsed_vaa.sequence))
], messenger.programId)[0];
// Account where the core bridge stored the vaa after the signatures checked out
let coreBridgeVaaKey = findProgramAddressSync([
Buffer.from("PostedVAA"),
Buffer.from(vaaHash, 'hex')
], new anchor.web3.PublicKey(srcNetwork.bridgeAddress))[0]
let configAcc = findProgramAddressSync([Buffer.from("config")], messenger.programId)[0];
//Confirm via Messenger Code
// will set the current message to the one in the vaa
const tx = await messenger.methods
.confirmMsg()
.accounts({
payer: KEYPAIR.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
processedVaa: processedVaaKey,
emitterAcc: emitterAddressAcc,
coreBridgeVaa: coreBridgeVaaKey,
config: configAcc
})
.rpc({skipPreflight: true});
return tx;
}
export async function getCurrentMsg(src:string){
const srcNetwork = config.networks[src];
let srcDeploymentInfo;
try{
srcDeploymentInfo = JSON.parse(fs.readFileSync(`./deployinfo/${src}.deploy.json`).toString());
} catch (e){
throw new Error(`${src} is not deployed yet`);
}
const messenger = new anchor.Program<MessengerTypes>(
IDL,
CONTRACT_ADDRESS,
new anchor.AnchorProvider(
new anchor.web3.Connection(srcNetwork['rpc']),
new anchor.Wallet(KEYPAIR),
{}
));
const configAcc = findProgramAddressSync([
Buffer.from('config')
], messenger.programId)[0]
const configAccountData = await messenger.account.config.fetch(configAcc);
return configAccountData.currentMsg;
}
function getVaaHash(parsed_vaa){
//Create VAA Hash to use in core bridge key
let buffer_array = []
buffer_array.push(byteify.serializeUint32(parsed_vaa.timestamp));
buffer_array.push(byteify.serializeUint32(parsed_vaa.nonce));
buffer_array.push(byteify.serializeUint16(parsed_vaa.emitter_chain));
buffer_array.push(Uint8Array.from(parsed_vaa.emitter_address));
buffer_array.push(byteify.serializeUint64(parsed_vaa.sequence));
buffer_array.push(byteify.serializeUint8(parsed_vaa.consistency_level));
buffer_array.push(Uint8Array.from(parsed_vaa.payload));
const hash = keccak256(Buffer.concat(buffer_array));
return hash.toString("hex");
}