diff --git a/evm/ts/src/ntt.ts b/evm/ts/src/ntt.ts index 2bb886b79..47af29766 100644 --- a/evm/ts/src/ntt.ts +++ b/evm/ts/src/ntt.ts @@ -1,6 +1,7 @@ import { Chain, Network, + encoding, nativeChainIds, toChainId, } from "@wormhole-foundation/sdk-base"; @@ -9,10 +10,12 @@ import { ChainAddress, ChainsConfig, Contracts, + canonicalAddress, serialize, + toUniversal, universalAddress, } from "@wormhole-foundation/sdk-definitions"; -import type { EvmChains, EvmPlatformType } from "@wormhole-foundation/sdk-evm"; +import type { AnyEvmAddress, EvmChains, EvmPlatformType } from "@wormhole-foundation/sdk-evm"; import { EvmAddress, EvmPlatform, @@ -36,8 +39,7 @@ import { } from "./bindings.js"; export class EvmNttWormholeTranceiver - implements NttTransceiver -{ + implements NttTransceiver { transceiver: NttTransceiverBindings.NttTransceiver; constructor( readonly manager: EvmNtt, @@ -50,17 +52,59 @@ export class EvmNttWormholeTranceiver ); } + getAddress(): ChainAddress { + return { chain: this.manager.chain, address: toUniversal(this.manager.chain, this.address) }; + } + encodeFlags(flags: { skipRelay: boolean }): Uint8Array { return new Uint8Array([flags.skipRelay ? 1 : 0]); } - async *setPeer(peer: ChainAddress) { + async *setPeer

(peer: ChainAddress

): AsyncGenerator> { const tx = await this.transceiver.setWormholePeer.populateTransaction( toChainId(peer.chain), universalAddress(peer) ); yield this.manager.createUnsignedTx(tx, "WormholeTransceiver.registerPeer"); } + + async getPauser(): Promise | null> { + const pauser = await this.transceiver.pauser(); + return new EvmAddress(pauser) as AccountAddress; + } + + async *setPauser(pauser: AccountAddress) { + const canonicalPauser = canonicalAddress({chain: this.manager.chain, address: pauser}); + const tx = await this.transceiver.transferPauserCapability.populateTransaction(canonicalPauser); + yield this.manager.createUnsignedTx(tx, "WormholeTransceiver.setPauser"); + } + + async getPeer(chain: C): Promise | null> { + const peer = await this.transceiver.getWormholePeer(toChainId(chain)); + const peerAddress = encoding.hex.decode(peer); + const zeroAddress = new Uint8Array(32); + if (encoding.bytes.equals(zeroAddress, peerAddress)) { + return null; + } + + return { + chain: chain, + address: toUniversal(chain, peerAddress), + }; + } + + async isEvmChain(chain: Chain): Promise { + return await this.transceiver.isWormholeEvmChain(toChainId(chain)); + } + + async *setIsEvmChain(chain: Chain, isEvm: boolean) { + const tx = await this.transceiver.setIsWormholeEvmChain.populateTransaction( + toChainId(chain), + isEvm + ); + yield this.manager.createUnsignedTx(tx, "WormholeTransceiver.setIsEvmChain"); + } + async *receive(attestation: WormholeNttTransceiver.VAA) { const tx = await this.transceiver.receiveMessage.populateTransaction( serialize(attestation) @@ -77,6 +121,17 @@ export class EvmNttWormholeTranceiver ); } + async *setIsWormholeRelayingEnabled(destChain: Chain, enabled: boolean) { + const tx = await this.transceiver.setIsWormholeRelayingEnabled.populateTransaction( + toChainId(destChain), + enabled + ); + yield this.manager.createUnsignedTx( + tx, + "WormholeTransceiver.setWormholeRelayingEnabled" + ); + } + async isSpecialRelayingEnabled(destChain: Chain): Promise { return await this.transceiver.isSpecialRelayingEnabled( toChainId(destChain) @@ -85,8 +140,7 @@ export class EvmNttWormholeTranceiver } export class EvmNtt - implements Ntt -{ + implements Ntt { tokenAddress: string; readonly chainId: bigint; manager: NttManagerBindings.NttManager; @@ -117,14 +171,66 @@ export class EvmNtt this.provider ); - this.xcvrs = [ - // Enable more Transceivers here - new EvmNttWormholeTranceiver( - this, - contracts.ntt.transceiver.wormhole!, - abiBindings! - ), - ]; + if (contracts.ntt.transceiver.wormhole != null) { + this.xcvrs = [ + // Enable more Transceivers here + new EvmNttWormholeTranceiver( + this, + contracts.ntt.transceiver.wormhole, + abiBindings! + ), + ]; + } else { + this.xcvrs = []; + } + } + + async getTransceiver(ix: number): Promise | null> { + // TODO: should we make an RPC call here, or just trust that the xcvrs are set up correctly? + return this.xcvrs[ix] || null; + } + + async getMode(): Promise { + const mode: bigint = await this.manager.getMode(); + return mode === 0n ? "locking" : "burning"; + } + + async isPaused(): Promise { + return await this.manager.isPaused(); + } + + async *pause() { + const tx = await this.manager.pause.populateTransaction() + yield this.createUnsignedTx(tx, "Ntt.pause"); + } + + async *unpause() { + const tx = await this.manager.unpause.populateTransaction() + yield this.createUnsignedTx(tx, "Ntt.unpause"); + } + + async getOwner(): Promise> { + return new EvmAddress(await this.manager.owner()) as AccountAddress; + } + + async getPauser(): Promise | null> { + return new EvmAddress(await this.manager.pauser()) as AccountAddress; + } + + async *setOwner(owner: AnyEvmAddress) { + const canonicalOwner = new EvmAddress(owner).toString(); + const tx = await this.manager.transferOwnership.populateTransaction(canonicalOwner); + yield this.createUnsignedTx(tx, "Ntt.setOwner"); + } + + async *setPauser(pauser: AnyEvmAddress) { + const canonicalPauser = new EvmAddress(pauser).toString(); + const tx = await this.manager.transferPauserCapability.populateTransaction(canonicalPauser); + yield this.createUnsignedTx(tx, "Ntt.setPauser"); + } + + async getThreshold(): Promise { + return Number(await this.manager.getThreshold()); } async isRelayingAvailable(destination: Chain): Promise { @@ -187,6 +293,21 @@ export class EvmNtt ); } + async getPeer(chain: C): Promise | null> { + const peer = await this.manager.getPeer(toChainId(chain)); + const peerAddress = encoding.hex.decode(peer.peerAddress); + const zeroAddress = new Uint8Array(32); + if (encoding.bytes.equals(zeroAddress, peerAddress)) { + return null; + } + + return { + address: { chain: chain, address: toUniversal(chain, peerAddress) }, + tokenDecimals: Number(peer.tokenDecimals), + inboundLimit: await this.getInboundLimit(chain), + }; + } + static async fromRpc( provider: Provider, config: ChainsConfig @@ -342,10 +463,39 @@ export class EvmNtt return await this.manager.getCurrentOutboundCapacity(); } + async getOutboundLimit(): Promise { + const encoded: EncodedTrimmedAmount = (await this.manager.getOutboundLimitParams()).limit; + const trimmedAmount: TrimmedAmount = decodeTrimmedAmount(encoded); + const tokenDecimals = await this.getTokenDecimals(); + + return untrim(trimmedAmount, tokenDecimals); + } + + async *setOutboundLimit(limit: bigint) { + const tx = await this.manager.setOutboundLimit.populateTransaction(limit); + yield this.createUnsignedTx(tx, "Ntt.setOutboundLimit"); + } + async getCurrentInboundCapacity(fromChain: Chain): Promise { return await this.manager.getCurrentInboundCapacity(toChainId(fromChain)); } + async getInboundLimit(fromChain: Chain): Promise { + const encoded: EncodedTrimmedAmount = (await this.manager.getInboundLimitParams(toChainId(fromChain))).limit; + const trimmedAmount: TrimmedAmount = decodeTrimmedAmount(encoded); + const tokenDecimals = await this.getTokenDecimals(); + + return untrim(trimmedAmount, tokenDecimals); + } + + async *setInboundLimit(fromChain: Chain, limit: bigint) { + const tx = await this.manager.setInboundLimit.populateTransaction( + limit, + toChainId(fromChain) + ); + yield this.createUnsignedTx(tx, "Ntt.setInboundLimit"); + } + async getRateLimitDuration(): Promise { return await this.manager.rateLimitDuration(); } @@ -381,6 +531,40 @@ export class EvmNtt yield this.createUnsignedTx(tx, "Ntt.completeInboundQueuedTransfer"); } + async verifyAddresses(): Promise | null> { + const local: Partial = { + manager: this.managerAddress, + token: this.tokenAddress, + transceiver: { + wormhole: this.xcvrs[0]?.address, + }, + // TODO: what about the quoter? + }; + + const remote: Partial = { + manager: this.managerAddress, + token: await this.manager.token(), + transceiver: { + wormhole: (await this.manager.getTransceivers())[0]! // TODO: make this more generic + }, + }; + + const deleteMatching = (a: any, b: any) => { + for (const k in a) { + if (typeof a[k] === "object") { + deleteMatching(a[k], b[k]); + if (Object.keys(a[k]).length === 0) delete a[k]; + } else if (a[k] === b[k]) { + delete a[k]; + } + } + } + + deleteMatching(remote, local); + + return Object.keys(remote).length > 0 ? remote : null; + } + createUnsignedTx( txReq: TransactionRequest, description: string, @@ -395,3 +579,36 @@ export class EvmNtt ); } } + +type EncodedTrimmedAmount = bigint; // uint72 + +type TrimmedAmount = { + amount: bigint; + decimals: number; +}; + +function decodeTrimmedAmount(encoded: EncodedTrimmedAmount): TrimmedAmount { + const decimals = Number(encoded & 0xffn); + const amount = encoded >> 8n; + return { + amount, + decimals, + }; +} + +function untrim(trimmed: TrimmedAmount, toDecimals: number): bigint { + const { amount, decimals: fromDecimals } = trimmed; + return scale(amount, fromDecimals, toDecimals); +} + +function scale(amount: bigint, fromDecimals: number, toDecimals: number): bigint { + if (fromDecimals == toDecimals) { + return amount; + } + + if (fromDecimals > toDecimals) { + return amount / (10n ** BigInt(fromDecimals - toDecimals)); + } else { + return amount * (10n ** BigInt(toDecimals - fromDecimals)); + } +} diff --git a/sdk/__tests__/utils.ts b/sdk/__tests__/utils.ts index be88dfba8..579a9bf2f 100644 --- a/sdk/__tests__/utils.ts +++ b/sdk/__tests__/utils.ts @@ -127,7 +127,7 @@ export async function link(chainInfos: Ctx[]) { chain: hubChain, emitter: Wormhole.chainAddress( hubChain, - hub.contracts!.transceiver.wormhole + hub.contracts!.transceiver.wormhole! ).address.toUniversalAddress(), sequence: 0n, }; @@ -595,7 +595,7 @@ async function setupPeer(targetCtx: Ctx, peerCtx: Ctx) { } = peerCtx.contracts!; const peerManager = Wormhole.chainAddress(peer.chain, manager); - const peerTransceiver = Wormhole.chainAddress(peer.chain, transceiver); + const peerTransceiver = Wormhole.chainAddress(peer.chain, transceiver!); const tokenDecimals = target.config.nativeTokenDecimals; const inboundLimit = amount.units(amount.parse("1000", tokenDecimals)); @@ -625,7 +625,7 @@ async function setupPeer(targetCtx: Ctx, peerCtx: Ctx) { ) { const nativeSigner = (signer as NativeSigner).unwrap(); const xcvr = WormholeTransceiver__factory.connect( - targetCtx.contracts!.transceiver.wormhole, + targetCtx.contracts!.transceiver.wormhole!, nativeSigner.signer ); const peerChainId = toChainId(peer.chain); diff --git a/sdk/definitions/src/ntt.ts b/sdk/definitions/src/ntt.ts index 64f0ee997..96f021e31 100644 --- a/sdk/definitions/src/ntt.ts +++ b/sdk/definitions/src/ntt.ts @@ -38,7 +38,7 @@ export namespace Ntt { token: string; manager: string; transceiver: { - wormhole: string; + wormhole?: string; }; quoter?: string; }; @@ -87,6 +87,12 @@ export namespace Ntt { payload: Uint8Array; }; + export type Peer = { + address: ChainAddress; + tokenDecimals: number; + inboundLimit: bigint; + }; + // TODO: should layoutify this but couldnt immediately figure out how to // specify the length of the array as an encoded value export function encodeTransceiverInstructions(ixs: TransceiverInstruction[]) { @@ -153,12 +159,35 @@ export namespace Ntt { * @typeparam C the chain */ export interface Ntt { + getMode(): Promise; + + isPaused(): Promise; + + pause( + payer?: AccountAddress + ): AsyncGenerator>; + + unpause( + payer?: AccountAddress + ): AsyncGenerator>; + + getOwner(): Promise>; + + getPauser(): Promise | null>; + + setOwner(newOwner: AccountAddress, payer?: AccountAddress): AsyncGenerator>; + + setPauser(newOwner: AccountAddress, payer?: AccountAddress): AsyncGenerator>; + + getThreshold(): Promise; + setPeer( peer: ChainAddress, tokenDecimals: number, inboundLimit: bigint, payer?: AccountAddress ): AsyncGenerator>; + setWormholeTransceiverPeer( peer: ChainAddress, payer?: AccountAddress @@ -209,10 +238,26 @@ export interface Ntt { /** Get the number of decimals associated with the token under management */ getTokenDecimals(): Promise; + /** Get the peer information for the given chain if it exists */ + getPeer(chain: C): Promise | null>; + + getTransceiver(ix: number): Promise | null>; + /** * getCurrentOutboundCapacity returns the current outbound capacity of the Ntt manager */ getCurrentOutboundCapacity(): Promise; + + /** + * getOutboundLimit returns the maximum outbound capacity of the Ntt manager + */ + getOutboundLimit(): Promise; + + /** + * setOutboundLimit sets the maximum outbound capacity of the Ntt manager + */ + setOutboundLimit(limit: bigint, payer?: AccountAddress): AsyncGenerator>; + /** * getCurrentInboundCapacity returns the current inbound capacity of the Ntt manager * @param fromChain the chain to check the inbound capacity for @@ -224,6 +269,18 @@ export interface Ntt { */ getRateLimitDuration(): Promise; + /** + * getInboundLimit returns the maximum inbound capacity of the Ntt manager + * @param fromChain the chain to check the inbound limit for + */ + getInboundLimit(fromChain: Chain): Promise; + + setInboundLimit( + fromChain: Chain, + limit: bigint, + payer?: AccountAddress + ): AsyncGenerator>; + /** * getIsApproved returns whether an attestation is approved * an attestation is approved when it has been validated but has not necessarily @@ -267,6 +324,21 @@ export interface Ntt { transceiverMessage: Ntt.Message, payer?: AccountAddress ): AsyncGenerator>; + + /** + * Given a manager address, the rest of the addresses (token address and + * transceiver addresses) can be queried from the manager contract directly. + * This method verifies that the addresses that were used to construct the Ntt + * instance match the addresses that are stored in the manager contract. + * + * TODO: perhaps a better way to do this would be by allowing async protocol + * initializers so this can be done when constructing the Ntt instance. + * That would be a larger change (in the connect sdk) so we do this for now. + * + * @returns the addresses that don't match the expected addresses, or null if + * they all match + */ + verifyAddresses(): Promise | null>; } export interface NttTransceiver< @@ -274,10 +346,19 @@ export interface NttTransceiver< C extends Chain, A extends Ntt.Attestation > { + + getAddress(): ChainAddress; + /** setPeer sets a peer address for a given chain * Note: Admin only */ - setPeer(peer: ChainAddress): AsyncGenerator>; + setPeer(peer: ChainAddress, payer?: AccountAddress): AsyncGenerator>; + + getPeer(chain: C): Promise | null>; + + setPauser(newPauser: AccountAddress, payer?: AccountAddress): AsyncGenerator>; + + getPauser(): Promise | null>; /** * receive calls the `receive*` method on the transceiver diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 054797d89..7ab6fde0d 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -723,6 +723,23 @@ export namespace NTT { return transferIx; } + export async function createTransferOwnershipInstruction( + program: Program>, + args: { + newOwner: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return await program.methods + .transferOwnership() + .accounts({ + config: pdas.configAccount(), + newOwner: args.newOwner, + }) + .instruction(); + } + export async function createSetPeerInstruction( program: Program>, args: { @@ -753,6 +770,25 @@ export namespace NTT { .instruction(); } + // TODO: untested + export async function createSetPausedInstruction( + program: Program>, + args: { + owner: PublicKey; + paused: boolean; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return await program.methods + .setPaused(args.paused) + .accountsStrict({ + owner: args.owner, + config: pdas.configAccount(), + }) + .instruction(); + } + export async function setWormholeTransceiverPeer( program: Program>, args: { @@ -862,11 +898,10 @@ export namespace NTT { }; } - export async function createSetOuboundLimitInstruction( + export async function createSetOutboundLimitInstruction( program: Program>, args: { owner: PublicKey; - chain: Chain; limit: BN; }, pdas?: Pdas diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 742b60706..e80873ee1 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -21,12 +21,15 @@ import { Contracts, NativeAddress, UnsignedTransaction, + toUniversal, } from "@wormhole-foundation/sdk-definitions"; import { Ntt, + NttTransceiver, WormholeNttTransceiver, } from "@wormhole-foundation/sdk-definitions-ntt"; import { + AnySolanaAddress, SolanaAddress, SolanaChains, SolanaPlatform, @@ -43,9 +46,55 @@ import { NTT, NttQuoter, WEI_PER_GWEI } from "../lib/index.js"; import { IdlVersion, NttBindings, getNttProgram } from "../lib/bindings.js"; +export class SolanaNttWormholeTransceiver + implements NttTransceiver { + + constructor( + readonly manager: SolanaNtt, + readonly address: PublicKey + ) {} + + async getPauser(): Promise | null> { + return null + } + + async *setPauser(_newPauser: AccountAddress, _payer: AccountAddress) { + throw new Error("Method not implemented."); + } + + async *receive(_attestation: WormholeNttTransceiver.VAA) { + // TODO: this is implemented below (in the transceiver code). it could get + // tricky in general with multiple transceivers, as they might return an + // instruction, or multiple instructions, etc. + // in any case, we should implement this here. + throw new Error("Method not implemented."); + } + + getAddress(): ChainAddress { + return { chain: this.manager.chain, address: toUniversal(this.manager.chain, this.address.toBase58()) }; + } + + async *setPeer(peer: ChainAddress, payer: AccountAddress) { + yield* this.manager.setWormholeTransceiverPeer(peer, payer); + } + + async getPeer(chain: C): Promise | null> { + const peer = + await this.manager.program.account.transceiverPeer.fetchNullable( + this.manager.pdas.transceiverPeerAccount(chain) + ); + + if (!peer) return null; + + return { + chain, + address: toUniversal(chain, new Uint8Array(peer.address)), + }; + } +} + export class SolanaNtt - implements Ntt -{ + implements Ntt { core: SolanaWormholeCore; pdas: NTT.Pdas; @@ -55,6 +104,12 @@ export class SolanaNtt quoter?: NttQuoter; addressLookupTable?: AddressLookupTableAccount; + // NOTE: these are stored from the constructor, but are not used directly + // (only in verifyAddresses) + private managerAddress: string; + private tokenAddress: string; + private whTransceiverAddress?: string; + constructor( readonly network: N, readonly chain: C, @@ -70,6 +125,10 @@ export class SolanaNtt version as IdlVersion ); + this.managerAddress = contracts.ntt.manager; + this.tokenAddress = contracts.ntt.token; + this.whTransceiverAddress = contracts.ntt.transceiver.wormhole; + if (this.contracts.ntt?.quoter) this.quoter = new NttQuoter( connection, @@ -86,6 +145,79 @@ export class SolanaNtt this.pdas = NTT.pdas(this.program.programId); } + async getTransceiver(ix: number): Promise | null> { + if (ix !== 0) return null; + if (this.whTransceiverAddress === undefined) return null; + + return new SolanaNttWormholeTransceiver(this, new PublicKey(this.whTransceiverAddress)); + } + + async getMode(): Promise { + const config = await this.getConfig(); + return config.mode.locking != null ? "locking" : "burning"; + } + + async isPaused(): Promise { + const config = await this.getConfig(); + return config.paused; + } + + async *pause(payer: AccountAddress) { + const sender = new SolanaAddress(payer).unwrap(); + const ix = await NTT.createSetPausedInstruction(this.program, { + owner: sender, + paused: true, + }); + + const tx = new Transaction(); + tx.feePayer = sender; + tx.add(ix); + yield this.createUnsignedTx({ transaction: tx }, "Ntt.Pause"); + } + + async *unpause(payer: AccountAddress) { + const sender = new SolanaAddress(payer).unwrap(); + const ix = await NTT.createSetPausedInstruction(this.program, { + owner: sender, + paused: false, + }); + + const tx = new Transaction(); + tx.feePayer = sender; + tx.add(ix); + yield this.createUnsignedTx({ transaction: tx }, "Ntt.Unpause"); + } + + async getThreshold(): Promise { + const config = await this.getConfig(); + return config.threshold + } + + async getOwner(): Promise> { + const config = await this.getConfig(); + return new SolanaAddress(config.owner) as AccountAddress; + } + + async getPauser(): Promise | null> { + return null + } + + async *setOwner(newOwner: AnySolanaAddress, payer: AccountAddress) { + const sender = new SolanaAddress(payer).unwrap(); + const ix = await NTT.createTransferOwnershipInstruction(this.program, { + newOwner: new SolanaAddress(newOwner).unwrap(), + }); + + const tx = new Transaction(); + tx.feePayer = sender; + tx.add(ix); + yield this.createUnsignedTx({ transaction: tx }, "Ntt.SetOwner"); + } + + async *setPauser(_newPauser: AnySolanaAddress, _payer: AccountAddress) { + throw new Error("Pauser role not supported on Solna."); + } + async isRelayingAvailable(destination: Chain): Promise { if (!this.quoter) return false; return await this.quoter.isRelayEnabled(destination); @@ -147,6 +279,18 @@ export class SolanaNtt ); } + async getPeer(chain: C): Promise | null> { + const peer = await this.program.account.nttManagerPeer.fetchNullable(this.pdas.peerAccount(chain)); + + if (!peer) return null; + + return { + address: { chain: chain, address: toUniversal(chain, new Uint8Array(peer.address)) }, + tokenDecimals: peer.tokenDecimals, + inboundLimit: await this.getInboundLimit(chain), + }; + } + async getCustodyAddress(): Promise { return (await this.getConfig()).custody.toBase58(); } @@ -156,11 +300,17 @@ export class SolanaNtt contracts: Contracts & { ntt: Ntt.Contracts }, sender?: AccountAddress ): Promise { - return NTT.getVersion( - connection, - new PublicKey(contracts.ntt.manager!), - sender ? new SolanaAddress(sender).unwrap() : undefined - ); + try { + return await NTT.getVersion( + connection, + new PublicKey(contracts.ntt.manager!), + sender ? new SolanaAddress(sender).unwrap() : undefined + ); + } catch (e) { + // This might happen if e.g. the program is not deployed yet. + const version = "2.0.0" + return version; + } } async *initialize( @@ -352,17 +502,17 @@ export class SolanaNtt const transferIx = config.mode.locking != null ? NTT.createTransferLockInstruction( - this.program, - config, - txArgs, - this.pdas - ) + this.program, + config, + txArgs, + this.pdas + ) : NTT.createTransferBurnInstruction( - this.program, - config, - txArgs, - this.pdas - ); + this.program, + config, + txArgs, + this.pdas + ); const releaseIx = NTT.createReleaseOutboundInstruction( this.program, @@ -493,6 +643,7 @@ export class SolanaNtt revertOnDelay: false, }; + // TODO: loop through transceivers etc. const redeemIx = NTT.createRedeemInstruction(this.program, config, { payer: senderAddress, vaa: wormholeNTT, @@ -501,15 +652,15 @@ export class SolanaNtt const releaseIx = config.mode.locking != null ? NTT.createReleaseInboundUnlockInstruction( - this.program, - config, - releaseArgs - ) + this.program, + config, + releaseArgs + ) : NTT.createReleaseInboundMintInstruction( - this.program, - config, - releaseArgs - ); + this.program, + config, + releaseArgs + ); const tx = new Transaction(); tx.feePayer = senderAddress; @@ -538,6 +689,26 @@ export class SolanaNtt return BigInt(rl.rateLimit.capacityAtLastTx.toString()); } + async getOutboundLimit(): Promise { + const rl = await this.program.account.outboxRateLimit.fetch( + this.pdas.outboxRateLimitAccount() + ); + return BigInt(rl.rateLimit.limit.toString()); + } + + async *setOutboundLimit(limit: bigint, payer: AccountAddress) { + const sender = new SolanaAddress(payer).unwrap(); + const ix = await NTT.createSetOutboundLimitInstruction(this.program, { + owner: sender, + limit: new BN(limit.toString()), + }); + + const tx = new Transaction(); + tx.feePayer = sender; + tx.add(ix); + yield this.createUnsignedTx({ transaction: tx }, "Ntt.SetOutboundLimit"); + } + async getCurrentInboundCapacity(fromChain: Chain): Promise { const rl = await this.program.account.inboxRateLimit.fetch( this.pdas.inboxRateLimitAccount(fromChain) @@ -550,6 +721,31 @@ export class SolanaNtt return BigInt(24 * 60 * 60); } + async getInboundLimit(fromChain: Chain): Promise { + const rl = await this.program.account.inboxRateLimit.fetch( + this.pdas.inboxRateLimitAccount(fromChain) + ); + return BigInt(rl.rateLimit.limit.toString()); + } + + async *setInboundLimit( + fromChain: Chain, + limit: bigint, + payer: AccountAddress + ) { + const sender = new SolanaAddress(payer).unwrap(); + const ix = await NTT.setInboundLimit(this.program, { + owner: sender, + chain: fromChain, + limit: new BN(limit.toString()), + }); + + const tx = new Transaction(); + tx.feePayer = sender; + tx.add(ix); + yield this.createUnsignedTx({ transaction: tx }, "Ntt.SetInboundLimit"); + } + async getIsExecuted(attestation: Ntt.Attestation): Promise { if (attestation.payloadName !== "WormholeTransfer") return false; const payload = attestation.payload["nttManagerPayload"]; @@ -622,15 +818,15 @@ export class SolanaNtt tx.add( await (config.mode.locking != null ? NTT.createReleaseInboundUnlockInstruction( - this.program, - config, - releaseArgs - ) + this.program, + config, + releaseArgs + ) : NTT.createReleaseInboundMintInstruction( - this.program, - config, - releaseArgs - )) + this.program, + config, + releaseArgs + )) ); yield this.createUnsignedTx( @@ -668,6 +864,37 @@ export class SolanaNtt return null; } + async verifyAddresses(): Promise | null> { + const local: Partial = { + manager: this.managerAddress, + token: this.tokenAddress, + transceiver: { + wormhole: this.whTransceiverAddress, + }, + }; + + const remote: Partial = { + manager: this.program.programId.toBase58(), + token: (await this.getConfig()).mint.toBase58(), + transceiver: { wormhole: this.pdas.emitterAccount().toBase58() }, + }; + + const deleteMatching = (a: any, b: any) => { + for (const k in a) { + if (typeof a[k] === "object") { + deleteMatching(a[k], b[k]); + if (Object.keys(a[k]).length === 0) delete a[k]; + } else if (a[k] === b[k]) { + delete a[k]; + } + } + } + + deleteMatching(remote, local); + + return Object.keys(remote).length > 0 ? remote : null; + } + async getAddressLookupTable( useCache = true ): Promise {