Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 126 additions & 1 deletion cli/src/commands/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import type {
import { encoding } from "@wormhole-foundation/sdk-connect";
import {
Wormhole,
chains,
chainToPlatform,
isNetwork,
toUniversal,
type Chain,
type UnsignedTransaction,
} from "@wormhole-foundation/sdk";
import evm from "@wormhole-foundation/sdk/platforms/evm";
import solana from "@wormhole-foundation/sdk/platforms/solana";
Expand All @@ -18,14 +21,20 @@ import {
Keypair,
PublicKey,
SendTransactionError,
Transaction,
} from "@solana/web3.js";
import * as spl from "@solana/spl-token";

import { NTT, SolanaNtt } from "@wormhole-foundation/sdk-solana-ntt";
import type { SolanaChains } from "@wormhole-foundation/sdk-solana";
import {
SolanaAddress,
type SolanaChains,
} from "@wormhole-foundation/sdk-solana";

import { colors } from "../colors.js";
import { loadConfig, type Config } from "../deployments";
import { getSigner, type SignerType } from "../signers/getSigner";
import { newSignSendWaiter } from "../signers/signSendWait.js";
import { validatePayerOption } from "../validation";
import fs from "fs";

Expand Down Expand Up @@ -384,6 +393,122 @@ export function createSolanaCommand(
console.log(`Keypair: ${buildResult.programKeypairPath}`);
}
)
.command(
"claim-ownership",
"Claim ownership of a Solana NTT program after a 2-step ownership transfer",
(yargs: any) =>
yargs
.option("chain", {
describe: "Solana chain where ownership will be claimed",
type: "string",
choices: chains,
demandOption: true,
})
.option("path", options.deploymentPath)
.option("network", options.network)
.option("signer-type", options.signerType)
.example(
"$0 solana claim-ownership --chain Solana --network Mainnet",
"Claim ownership of the Solana NTT program"
),
async (argv: any) => {
const path = argv["path"];
const deployments: Config = loadConfig(path);
const chain: Chain = argv["chain"];
const network = argv["network"];
const signerType = argv["signer-type"] as SignerType;

if (!isNetwork(network)) {
console.error("Invalid network");
process.exit(1);
}

if (chainToPlatform(chain) !== "Solana") {
console.error(
`claim-ownership is only supported on Solana chains, got ${chain}`
);
process.exit(1);
}

const chainConfig = deployments.chains[chain];
if (!chainConfig) {
console.error(
`Chain ${chain} not found in deployment configuration`
);
process.exit(1);
}

console.log(colors.blue("🔑 Manual claimOwnership Operation"));
console.log(`Chain: ${colors.yellow(chain)}`);

try {
const manager = {
chain,
address: toUniversal(chain, chainConfig.manager),
};
const [, ctx, ntt] = await pullChainConfig(
network,
manager,
overrides
);
Comment on lines +415 to +453

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the deployment file's network as the source of truth.

This handler loads deployments, but then executes against argv["network"]. If those differ, you can pair the manager address from one environment with RPC/config from another and attempt the ownership claim on the wrong cluster. Also, this path skips validateChain(network, chain).

Suggested fix
             const path = argv["path"];
             const deployments: Config = loadConfig(path);
             const chain: Chain = argv["chain"];
-            const network = argv["network"];
+            const requestedNetwork = argv["network"];
+            const deploymentNetwork = deployments.network as Network;
             const signerType = argv["signer-type"] as SignerType;
 
+            const network = requestedNetwork ?? deploymentNetwork;
             if (!isNetwork(network)) {
               console.error("Invalid network");
               process.exit(1);
             }
+
+            if (
+              requestedNetwork !== undefined &&
+              requestedNetwork !== deploymentNetwork
+            ) {
+              console.error(
+                `Deployment config is for ${deploymentNetwork}, but --network was ${requestedNetwork}`
+              );
+              process.exit(1);
+            }
 
             if (chainToPlatform(chain) !== "Solana") {
               console.error(
                 `claim-ownership is only supported on Solana chains, got ${chain}`
               );
               process.exit(1);
             }
+
+            validateChain(network, chain);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/commands/solana.ts` around lines 415 - 453, The handler currently
reads the deployment via loadConfig(path) but then uses argv["network"] and
argv["chain"] when calling pullChainConfig, which can pair a manager address
from the deployment with the wrong RPC and also skips validateChain; change the
logic to derive the network from the loaded deployments (use deployments.network
or equivalent field) and pass that into validateChain(...) and
pullChainConfig(...) instead of argv["network"], ensuring the manager address
(created from deployments.chains[chain]) is used with the matching deployment
network; update references around loadConfig, deployments, argv["network"],
validateChain, and pullChainConfig to reflect this single source of truth.


const solanaNtt = ntt as SolanaNtt<
typeof ctx.config.network,
SolanaChains
>;

const signer = await getSigner(ctx, signerType);
Comment on lines +409 to +460

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't advertise ledger here if Solana immediately rejects it.

claim-ownership is Solana-only, but it reuses the generic signer option and then calls getSigner(), which hard-fails for Solana ledger signers. One of the exposed CLI paths is therefore unusable.

Suggested fix
-              .option("signer-type", options.signerType)
+              .option("signer-type", {
+                ...options.signerType,
+                choices: ["privateKey"] as const,
+                default: "privateKey",
+              })

If you want to keep the shared option shape, add an explicit preflight check before getSigner() instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.option("signer-type", options.signerType)
.example(
"$0 solana claim-ownership --chain Solana --network Mainnet",
"Claim ownership of the Solana NTT program"
),
async (argv: any) => {
const path = argv["path"];
const deployments: Config = loadConfig(path);
const chain: Chain = argv["chain"];
const network = argv["network"];
const signerType = argv["signer-type"] as SignerType;
if (!isNetwork(network)) {
console.error("Invalid network");
process.exit(1);
}
if (chainToPlatform(chain) !== "Solana") {
console.error(
`claim-ownership is only supported on Solana chains, got ${chain}`
);
process.exit(1);
}
const chainConfig = deployments.chains[chain];
if (!chainConfig) {
console.error(
`Chain ${chain} not found in deployment configuration`
);
process.exit(1);
}
console.log(colors.blue("🔑 Manual claimOwnership Operation"));
console.log(`Chain: ${colors.yellow(chain)}`);
try {
const manager = {
chain,
address: toUniversal(chain, chainConfig.manager),
};
const [, ctx, ntt] = await pullChainConfig(
network,
manager,
overrides
);
const solanaNtt = ntt as SolanaNtt<
typeof ctx.config.network,
SolanaChains
>;
const signer = await getSigner(ctx, signerType);
.option("signer-type", {
...options.signerType,
choices: ["privateKey"] as const,
default: "privateKey",
})
.example(
"$0 solana claim-ownership --chain Solana --network Mainnet",
"Claim ownership of the Solana NTT program"
),
async (argv: any) => {
const path = argv["path"];
const deployments: Config = loadConfig(path);
const chain: Chain = argv["chain"];
const network = argv["network"];
const signerType = argv["signer-type"] as SignerType;
if (!isNetwork(network)) {
console.error("Invalid network");
process.exit(1);
}
if (chainToPlatform(chain) !== "Solana") {
console.error(
`claim-ownership is only supported on Solana chains, got ${chain}`
);
process.exit(1);
}
const chainConfig = deployments.chains[chain];
if (!chainConfig) {
console.error(
`Chain ${chain} not found in deployment configuration`
);
process.exit(1);
}
console.log(colors.blue("🔑 Manual claimOwnership Operation"));
console.log(`Chain: ${colors.yellow(chain)}`);
try {
const manager = {
chain,
address: toUniversal(chain, chainConfig.manager),
};
const [, ctx, ntt] = await pullChainConfig(
network,
manager,
overrides
);
const solanaNtt = ntt as SolanaNtt<
typeof ctx.config.network,
SolanaChains
>;
const signer = await getSigner(ctx, signerType);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/commands/solana.ts` around lines 409 - 460, The claim-ownership path
advertises the shared "signer-type" option but then calls getSigner() which will
hard-fail for Solana ledger signers; add a preflight check immediately before
calling getSigner() in the claim-ownership handler that inspects signerType
(from argv["signer-type"]) and the chain/platform (use chain or
chainToPlatform(chain)) and if signerType === "ledger" and the platform is
"Solana" print a clear error (e.g. "Ledger signer not supported for Solana
claim-ownership") and exit(1); this prevents invoking getSigner() with an
unsupported ledger type while keeping the shared option shape.

const newOwner = new SolanaAddress(
signer.address.address
).unwrap();

console.log(
`Signer/New Owner: ${colors.yellow(newOwner.toBase58())}`
);
console.log(
"\n" + colors.blue("Executing claimOwnership transaction...")
);

const ix = await NTT.createClaimOwnershipInstruction(
solanaNtt.program,
{ newOwner }
);

const tx = new Transaction();
tx.add(ix);
tx.feePayer = newOwner;

const txs = (async function* () {
yield solanaNtt.createUnsignedTx(
{ transaction: tx },
"Claim ownership"
) as UnsignedTransaction<any, any>;
})();

const signSendWaitFunc = newSignSendWaiter(undefined);
const results = await signSendWaitFunc(ctx, txs, signer.signer);

console.log(
`Transaction Hash: ${colors.green(
results[0]?.txid || results[0] || "Transaction completed"
)}`
);
console.log(
colors.green(
"\n✅ claimOwnership operation completed successfully!"
)
);
} catch (error) {
console.error(
colors.red("\n❌ claimOwnership operation failed:")
);
console.error(
error instanceof Error ? error.message : String(error)
);
process.exit(1);
}
}
)
.demandCommand();
},
handler: (_argv: any) => {},
Expand Down
Loading