@wormhole-foundation/sdk-algorand-core
Version:
SDK for Algorand, used in conjunction with @wormhole-foundation/sdk
291 lines • 14 kB
JavaScript
import { createVAA, encoding, keccak256, serialize, toChainId, } from "@wormhole-foundation/sdk-connect";
import { AlgorandAddress, AlgorandPlatform, AlgorandUnsignedTransaction, safeBigIntToNumber, } from "@wormhole-foundation/sdk-algorand";
import { LogicSigAccount, OnApplicationComplete, getApplicationAddress, makeApplicationCallTxnFromObject, makeApplicationOptInTxnFromObject, makePaymentTxnWithSuggestedParamsFromObject, modelsv2, signLogicSigTransaction, } from "algosdk";
import { SEED_AMT, StorageLogicSig } from "./storage.js";
export class AlgorandWormholeCore {
network;
chain;
connection;
contracts;
chainId;
coreAppId;
coreAppAddress;
tokenBridgeAppId;
tokenBridgeAppAddress;
static MAX_SIGS_PER_TXN = 6;
static ALGO_VERIFY_HASH = "EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A";
static ALGO_VERIFY = new Uint8Array([
6, 32, 4, 1, 0, 32, 20, 38, 1, 0, 49, 32, 50, 3, 18, 68, 49, 1, 35, 18, 68, 49, 16, 129, 6, 18,
68, 54, 26, 1, 54, 26, 3, 54, 26, 2, 136, 0, 3, 68, 34, 67, 53, 2, 53, 1, 53, 0, 40, 53, 240,
40, 53, 241, 52, 0, 21, 53, 5, 35, 53, 3, 35, 53, 4, 52, 3, 52, 5, 12, 65, 0, 68, 52, 1, 52, 0,
52, 3, 129, 65, 8, 34, 88, 23, 52, 0, 52, 3, 34, 8, 36, 88, 52, 0, 52, 3, 129, 33, 8, 36, 88, 7,
0, 53, 241, 53, 240, 52, 2, 52, 4, 37, 88, 52, 240, 52, 241, 80, 2, 87, 12, 20, 18, 68, 52, 3,
129, 66, 8, 53, 3, 52, 4, 37, 8, 53, 4, 66, 255, 180, 34, 137,
]);
// global state key for message fee
static feeKey = encoding.b64.encode("MessageFee");
// method selector for verifying a VAA
static verifyVaa = encoding.bytes.encode("verifyVAA");
// method selector for verifying signatures of a VAA
static verifySigs = encoding.bytes.encode("verifySigs");
// method selector string for publishing a message
static publishMessage = encoding.bytes.encode("publishMessage");
constructor(network, chain, connection, contracts) {
this.network = network;
this.chain = chain;
this.connection = connection;
this.contracts = contracts;
this.chainId = toChainId(chain);
if (!contracts.coreBridge) {
throw new Error(`Core contract address for chain ${chain} not found`);
}
const core = BigInt(contracts.coreBridge);
this.coreAppId = core;
this.coreAppAddress = getApplicationAddress(core);
if (!contracts.tokenBridge) {
throw new Error(`TokenBridge contract address for chain ${chain} not found`);
}
const tokenBridge = BigInt(contracts.tokenBridge);
this.tokenBridgeAppId = tokenBridge;
this.tokenBridgeAppAddress = getApplicationAddress(tokenBridge);
}
getGuardianSet(index) {
throw new Error("Method not implemented.");
}
async *verifyMessage(sender, vaa, appId) {
const address = new AlgorandAddress(sender).toString();
const txset = await AlgorandWormholeCore.submitVAAHeader(this.connection, this.coreAppId, appId ?? this.coreAppId, vaa, address);
for (const tx of txset.txs) {
yield this.createUnsignedTx(tx, "Core.verifyMessage");
}
}
static async fromRpc(connection, config) {
const [network, chain] = await AlgorandPlatform.chainFromRpc(connection);
const conf = config[chain];
if (conf.network !== network)
throw new Error(`Network mismatch: ${conf.network} !== ${network}`);
return new AlgorandWormholeCore(network, chain, connection, conf.contracts);
}
async *publishMessage(sender, message) {
// Call core bridge to publish message
const _sender = new AlgorandAddress(sender);
const address = _sender.toString();
const suggestedParams = await this.connection.getTransactionParams().do();
const storage = StorageLogicSig.forEmitter(this.coreAppId, _sender.toUint8Array());
const { accounts, txs } = await AlgorandWormholeCore.maybeCreateStorageTx(this.connection, address, this.coreAppId, storage, suggestedParams);
for (const tx of txs) {
yield this.createUnsignedTx(tx, "Core.publishMessage", true);
}
const act = makeApplicationCallTxnFromObject({
from: address,
appIndex: safeBigIntToNumber(this.coreAppId),
appArgs: [AlgorandWormholeCore.publishMessage, message, encoding.bignum.toBytes(0n, 8)],
accounts: accounts,
onComplete: OnApplicationComplete.NoOpOC,
suggestedParams,
});
yield this.createUnsignedTx({ tx: act }, "Core.publishMessage", true);
}
/**
* Return the message fee for the core bridge
* @param client An Algodv2 client
* @param bridgeId The application ID of the core bridge
* @returns Promise with the message fee for the core bridge
*/
async getMessageFee() {
const applInfoResp = await this.connection
.getApplicationByID(safeBigIntToNumber(this.coreAppId))
.do();
const appInfo = modelsv2.Application.from_obj_for_encoding(applInfoResp);
const val = appInfo.params.globalState?.find((kv) => kv.key === AlgorandWormholeCore.feeKey);
return val ? BigInt(val.value.uint) : 0n;
}
async getGuardianSetIndex() {
throw new Error("Not implemented");
}
async parseTransaction(txId) {
const result = await this.connection.pendingTransactionInformation(txId).do();
const ptr = modelsv2.PendingTransactionResponse.from_obj_for_encoding(result);
return this.parseTx(ptr).map((v) => {
return {
chain: v.emitterChain,
emitter: v.emitterAddress,
sequence: v.sequence,
};
});
}
async parseMessages(txId) {
const result = await this.connection.pendingTransactionInformation(txId).do();
const ptr = modelsv2.PendingTransactionResponse.from_obj_for_encoding(result);
return this.parseTx(ptr);
}
parseTx(ptr) {
const msgs = [];
if (ptr.innerTxns && ptr.innerTxns.length > 0) {
msgs.push(...ptr.innerTxns.flatMap((tx) => this.parseTx(tx)));
}
// Expect target is core app
if (BigInt(ptr.txn.txn.apid ?? 0) !== this.coreAppId)
return msgs;
// Expect logs
if (!ptr.logs || ptr.logs.length === 0)
return msgs;
// Expect publish messeage as first arg
const args = ptr.txn.txn.apaa ?? [];
if (args.length !== 3 ||
!encoding.bytes.equals(new Uint8Array(args[0]), AlgorandWormholeCore.publishMessage))
return msgs;
const sequence = encoding.bignum.decode(ptr.logs[0]);
const emitter = new AlgorandAddress(ptr.txn.txn.snd).toUniversalAddress();
const payload = new Uint8Array(args[1]);
const nonce = encoding.bignum.decode(args[2]);
msgs.push(createVAA("Uint8Array", {
emitterChain: this.chain,
emitterAddress: emitter,
sequence,
guardianSet: 0, // TODO: should we get this from the contract on init?
timestamp: 0, // TODO: Would need to get the full block to get the timestamp
consistencyLevel: 0,
nonce: Number(nonce),
payload,
signatures: [],
}));
return msgs;
}
/**
* Constructs opt in transactions
* @param client An Algodv2 client
* @param senderAddr Sender address
* @param appId Application ID
* @param storage StorageLogicSig
* @returns Address and array of TransactionSignerPairs
*/
static async maybeCreateStorageTx(client, senderAddr, appId, storage, suggestedParams) {
const appAddr = getApplicationAddress(appId);
const storageAddress = storage.address();
const txs = [];
if (await StorageLogicSig.storageAccountExists(client, storageAddress, appId))
return { accounts: [storageAddress], txs };
suggestedParams = suggestedParams ?? (await client.getTransactionParams().do());
// Pay the storage account some ALGO to min balance requirements
const seedTxn = makePaymentTxnWithSuggestedParamsFromObject({
from: senderAddr,
to: storageAddress,
amount: SEED_AMT,
suggestedParams,
});
seedTxn.fee = seedTxn.fee * 2;
txs.push({ tx: seedTxn });
// Opt in to the app and rekey to the app address that is using
// this as storage
const optinTxn = makeApplicationOptInTxnFromObject({
from: storageAddress,
appIndex: safeBigIntToNumber(appId),
rekeyTo: appAddr,
suggestedParams,
});
optinTxn.fee = 0;
txs.push({
tx: optinTxn,
signer: {
address: storage.address(),
signTxn: (txn) => Promise.resolve(signLogicSigTransaction(txn, storage).blob),
},
});
return {
accounts: [storageAddress],
txs,
};
}
/**
* Submits just the header of the VAA
* @param client AlgodV2 client
* @param bridgeId Application ID of the core bridge
* @param vaa The VAA (just the header is used)
* @param senderAddr Sending account address
* @param appid Application ID
* @returns Promise with current VAA state
*/
static async submitVAAHeader(client, coreId, appid, vaa, senderAddr, suggestedParams) {
suggestedParams = suggestedParams ?? (await client.getTransactionParams().do());
let txs = [];
// Get storage acct for message ID
const msgStorage = StorageLogicSig.forMessageId(appid, {
chain: vaa.emitterChain,
sequence: vaa.sequence,
emitter: vaa.emitterAddress,
});
const { accounts: seqAddr, txs: seqOptInTxs } = await AlgorandWormholeCore.maybeCreateStorageTx(client, senderAddr, appid, msgStorage, suggestedParams);
txs.push(...seqOptInTxs);
// Get storage account for Guardian set
const gsStorage = StorageLogicSig.forGuardianSet(coreId, vaa.guardianSet);
const { accounts: [storageAddr], txs: guardianOptInTxs, } = await AlgorandWormholeCore.maybeCreateStorageTx(client, senderAddr, coreId, gsStorage, suggestedParams);
txs.push(...guardianOptInTxs);
let accts = [...seqAddr, storageAddr];
// Get the Guardian keys
const keys = await StorageLogicSig.decodeLocalState(client, coreId, storageAddr);
// We don't pass the entire payload in but instead just pass it pre-digested. This gets around size
// limitations with lsigs AND reduces the cost of the entire operation on a congested network by reducing the
// bytes passed into the transaction
// This is a 2 pass digest
const digest = keccak256(vaa.hash);
// How many signatures can we process in a single txn... we can do 6!
// There are likely upwards of 19 signatures. So, we ned to split things up
const numSigs = vaa.signatures.length;
const numTxns = Math.ceil(numSigs / AlgorandWormholeCore.MAX_SIGS_PER_TXN);
const GuardianKeyLen = 20;
const lsa = new LogicSigAccount(AlgorandWormholeCore.ALGO_VERIFY);
for (let nt = 0; nt < numTxns; nt++) {
const step = nt * AlgorandWormholeCore.MAX_SIGS_PER_TXN;
const sigs = vaa.signatures.slice(step, step + AlgorandWormholeCore.MAX_SIGS_PER_TXN);
// The keyset is the set of Guardians that correspond
// to the current set of signatures in this loop.
// Each signature in 20 bytes and comes from decodeLocalState()
const arraySize = sigs.length * GuardianKeyLen;
const keySet = new Uint8Array(arraySize);
for (let i = 0; i < sigs.length; i++) {
// The first byte of the sig is the relative index of that signature in the signatures array
// Use that index to get the appropriate Guardian key
const sig = sigs[i];
const key = keys.slice(sig.guardianIndex * GuardianKeyLen + 1, (sig.guardianIndex + 1) * GuardianKeyLen + 1);
keySet.set(key, i * 20);
}
const appTxn = makeApplicationCallTxnFromObject({
appArgs: [
AlgorandWormholeCore.verifySigs,
encoding.bytes.concat(...sigs.map((s) => encoding.bytes.concat(new Uint8Array([s.guardianIndex]), s.signature.encode()))),
keySet,
digest,
],
accounts: accts,
appIndex: safeBigIntToNumber(coreId),
from: AlgorandWormholeCore.ALGO_VERIFY_HASH,
onComplete: OnApplicationComplete.NoOpOC,
suggestedParams,
});
appTxn.fee = 0;
txs.push({
tx: appTxn,
signer: {
address: lsa.address(),
signTxn: (txn) => Promise.resolve(signLogicSigTransaction(txn, lsa).blob),
},
});
}
const appTxn = makeApplicationCallTxnFromObject({
appArgs: [AlgorandWormholeCore.verifyVaa, serialize(vaa)],
accounts: accts,
appIndex: safeBigIntToNumber(coreId),
from: senderAddr,
onComplete: OnApplicationComplete.NoOpOC,
suggestedParams,
});
appTxn.fee = appTxn.fee * (2 + numTxns);
txs.push({ tx: appTxn });
return { accounts: accts, txs };
}
createUnsignedTx(txReq, description, parallelizable = true) {
return new AlgorandUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable);
}
}
//# sourceMappingURL=core.js.map