Skip to content

Commit 1a2a92e

Browse files
authored
cli: Add commands to create valid SPL Multisig and set mint authority (#628)
This PR adds: * Add `create_spl_multisig <multisigMemberPubkey...>` command that mimics `spl-token create-multisig` to create valid SPL Multisig. * Add `set-mint-authority <newAuthority>` command that sets mint authority based on version: * If version < `3.x.x`: calls `spl.setAuthority` directly * If version >= `3.x.x`: calls `acceptTokenAuthority` * Also handles cases where `newAuthority` is a valid SPL Multisig via the `--multisig` flag
1 parent 9141acb commit 1a2a92e

File tree

1 file changed

+314
-5
lines changed

1 file changed

+314
-5
lines changed

cli/src/index.ts

Lines changed: 314 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import chalk from "chalk";
1313
import yargs from "yargs";
1414
import { $ } from "bun";
1515
import { hideBin } from "yargs/helpers";
16-
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
16+
import { AddressLookupTableAccount, Connection, Keypair, PublicKey, SendTransactionError, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
1717
import * as spl from "@solana/spl-token";
1818
import fs from "fs";
1919
import readline from "readline";
@@ -785,7 +785,8 @@ yargs(hideBin(process.argv))
785785
continue;
786786
}
787787
const solanaNtt = ntt as SolanaNtt<Network, SolanaChains>;
788-
const tx = solanaNtt.initializeOrUpdateLUT({ payer: new SolanaAddress(signer.address.address).unwrap() })
788+
const payer = new SolanaAddress(signer.address.address).unwrap();
789+
const tx = solanaNtt.initializeOrUpdateLUT({ payer, owner: payer })
789790
try {
790791
await signSendWait(ctx, tx, signer.signer)
791792
} catch (e: any) {
@@ -963,6 +964,314 @@ yargs(hideBin(process.argv))
963964
const ata = spl.getAssociatedTokenAddressSync(mint, owner, true, tokenProgram);
964965
console.log(ata.toBase58());
965966
})
967+
.command("create-spl-multisig <multisigMemberPubkey...>",
968+
"create a valid SPL Multisig (see https://github.com/wormhole-foundation/native-token-transfers/tree/main/solana#spl-multisig-support for more info)",
969+
(yargs) =>
970+
yargs
971+
.positional("multisigMemberPubkey", {
972+
describe:
973+
"public keys of the members that can independently mint",
974+
type: "string",
975+
demandOption: true,
976+
})
977+
.option("path", options.deploymentPath)
978+
.option("payer", { ...options.payer, demandOption: true })
979+
.example(
980+
"$0 solana create-spl-multisig Sol1234... --payer <SOLANA_KEYPAIR_PATH>",
981+
"Create multisig with Sol1234... having independent mint privilege alongside NTT token-authority"
982+
)
983+
.example(
984+
"$0 solana create-spl-multisig Sol1234... Sol3456... Sol5678... --payer <SOLANA_KEYPAIR_PATH>",
985+
"Create multisig with Sol1234..., Sol3456..., and Sol5678... having mint privileges alongside NTT token-authority"
986+
),
987+
async (argv) => {
988+
const path = argv["path"];
989+
const deployments: Config = loadConfig(path);
990+
const chain: Chain = "Solana";
991+
const network = deployments.network as Network;
992+
993+
if (!fs.existsSync(argv["payer"])) {
994+
console.error("Payer not found. Specify with --payer");
995+
process.exit(1);
996+
}
997+
const payerKeypair = Keypair.fromSecretKey(
998+
new Uint8Array(
999+
JSON.parse(fs.readFileSync(argv["payer"]).toString())
1000+
)
1001+
);
1002+
1003+
if (!(chain in deployments.chains)) {
1004+
console.error(`Chain ${chain} not found in ${path}`);
1005+
process.exit(1);
1006+
}
1007+
const chainConfig = deployments.chains[chain]!;
1008+
const wh = new Wormhole(network, [solana.Platform, evm.Platform], overrides);
1009+
const ch = wh.getChain(chain);
1010+
const connection = await ch.getRpc();
1011+
const [, , ntt] = await pullChainConfig(
1012+
network,
1013+
{ chain, address: toUniversal(chain, chainConfig.manager) },
1014+
overrides
1015+
);
1016+
const solanaNtt = ntt as SolanaNtt<typeof network, SolanaChains>;
1017+
const tokenAuthority = NTT.pdas(chainConfig.manager).tokenAuthority();
1018+
1019+
// check if SPL-Multisig is supported for manager version
1020+
const major = Number(solanaNtt.version.split(".")[0]);
1021+
if (major < 3) {
1022+
console.error(
1023+
"SPL Multisig token mint authority is only supported for versions >= 3.x.x"
1024+
);
1025+
console.error("Use 'ntt upgrade' to upgrade the NTT contract to a specific version.");
1026+
process.exit(1);
1027+
}
1028+
1029+
try {
1030+
// use same tokenProgram as token to create multisig
1031+
const tokenProgram = (await solanaNtt.getConfig()).tokenProgram;
1032+
const additionalMemberPubkeys = (argv["multisigMemberPubkey"] as any).map(
1033+
(key: string) => new PublicKey(key)
1034+
);
1035+
const multisig = await spl.createMultisig(
1036+
connection,
1037+
payerKeypair,
1038+
[
1039+
tokenAuthority,
1040+
...additionalMemberPubkeys,
1041+
],
1042+
1,
1043+
undefined,
1044+
{ commitment: "finalized" },
1045+
tokenProgram
1046+
);
1047+
console.log(`Valid SPL Multisig created: ${multisig.toBase58()}`);
1048+
} catch (error) {
1049+
if (error instanceof Error) {
1050+
console.error(error.message);
1051+
} else if (error instanceof SendTransactionError) {
1052+
console.error(error.logs);
1053+
}
1054+
}
1055+
})
1056+
.command("set-mint-authority <newAuthority>",
1057+
"set token mint authority to token authority (or valid SPL Multisig if --multisig flag is provided)",
1058+
(yargs) =>
1059+
yargs
1060+
.positional("newAuthority", {
1061+
describe:
1062+
"token authority address (or valid SPL Multisig address if --multisig flag is provided)",
1063+
type: "string",
1064+
demandOption: true,
1065+
})
1066+
.option("path", options.deploymentPath)
1067+
.option("payer", { ...options.payer, demandOption: true })
1068+
.option("multisig", {
1069+
describe: "newAuthority is a valid SPL Multisig",
1070+
type: "boolean",
1071+
default: false,
1072+
})
1073+
.example(
1074+
"$0 solana set-mint-authority <TOKEN_AUTHORITY> --payer <SOLANA_KEYPAIR_PATH>",
1075+
"Set token mint authority to be the token authority address"
1076+
)
1077+
.example(
1078+
"$0 solana set-mint-authority <VALID_SPL_MULTISIG> --multisig --payer <SOLANA_KEYPAIR_PATH>",
1079+
"Set token mint authority to be a valid SPL Multisig"
1080+
),
1081+
async (argv) => {
1082+
const path = argv["path"];
1083+
const deployments: Config = loadConfig(path);
1084+
const chain: Chain = "Solana";
1085+
const network = deployments.network as Network;
1086+
1087+
if (!fs.existsSync(argv["payer"])) {
1088+
console.error("Payer not found. Specify with --payer");
1089+
process.exit(1);
1090+
}
1091+
const payerKeypair = Keypair.fromSecretKey(
1092+
new Uint8Array(
1093+
JSON.parse(fs.readFileSync(argv["payer"]).toString())
1094+
)
1095+
);
1096+
1097+
if (!(chain in deployments.chains)) {
1098+
console.error(`Chain ${chain} not found in ${path}`);
1099+
process.exit(1);
1100+
}
1101+
const chainConfig = deployments.chains[chain]!;
1102+
const wh = new Wormhole(network, [solana.Platform, evm.Platform], overrides);
1103+
const ch = wh.getChain(chain);
1104+
const connection: Connection = await ch.getRpc();
1105+
const [, , ntt] = await pullChainConfig(
1106+
network,
1107+
{ chain, address: toUniversal(chain, chainConfig.manager) },
1108+
overrides
1109+
);
1110+
const solanaNtt = ntt as SolanaNtt<typeof network, SolanaChains>;
1111+
const major = Number(solanaNtt.version.split(".")[0]);
1112+
const config = await solanaNtt.getConfig();
1113+
const tokenAuthority = NTT.pdas(chainConfig.manager).tokenAuthority();
1114+
1115+
// verify current mint authority is not token authority
1116+
const mintInfo = await spl.getMint(
1117+
connection,
1118+
config.mint,
1119+
undefined,
1120+
config.tokenProgram
1121+
);
1122+
if (!mintInfo.mintAuthority) {
1123+
console.error(
1124+
"Token has fixed supply and no further tokens may be minted"
1125+
);
1126+
process.exit(1);
1127+
}
1128+
if (mintInfo.mintAuthority.equals(tokenAuthority)) {
1129+
console.error("Please use https://github.com/wormhole-foundation/demo-ntt-token-mint-authority-transfer to transfer the token mint authority out of the NTT manager");
1130+
process.exit(1);
1131+
}
1132+
1133+
// verify current mint authority is not valid SPL Multisig
1134+
let isMultisigTokenAuthority = false;
1135+
try {
1136+
const multisigInfo = await spl.getMultisig(
1137+
connection,
1138+
mintInfo.mintAuthority,
1139+
undefined,
1140+
config.tokenProgram
1141+
);
1142+
if (multisigInfo.m === 1) {
1143+
const n = multisigInfo.n;
1144+
for (let i = 0; i < n; ++i) {
1145+
// TODO: not sure if there's an easier way to loop through and check
1146+
if ((multisigInfo[`signer${i + 1}` as keyof spl.Multisig] as PublicKey).equals(tokenAuthority)) {
1147+
isMultisigTokenAuthority = true;
1148+
break;
1149+
}
1150+
}
1151+
}
1152+
} catch {}
1153+
if (isMultisigTokenAuthority) {
1154+
console.error("Please use https://github.com/wormhole-foundation/demo-ntt-token-mint-authority-transfer to transfer the token mint authority out of the NTT manager");
1155+
process.exit(1);
1156+
}
1157+
1158+
// verify current mint authority is payer
1159+
if (!mintInfo.mintAuthority.equals(payerKeypair.publicKey)) {
1160+
console.error(
1161+
`Current mint authority (${mintInfo.mintAuthority.toBase58()}) does not match payer (${payerKeypair.publicKey.toBase58()}). Retry with current authority`
1162+
);
1163+
process.exit(1);
1164+
}
1165+
1166+
const isMultisig = argv["multisig"];
1167+
if (isMultisig) {
1168+
// check if SPL-Multisig is supported for manager version
1169+
if (major < 3) {
1170+
console.error(
1171+
"SPL Multisig token mint authority only supported for versions >= 3.x.x"
1172+
);
1173+
console.error("Use 'ntt upgrade' to upgrade the NTT contract to a specific version.");
1174+
process.exit(1);
1175+
}
1176+
}
1177+
1178+
// verify new authority address is valid
1179+
const newAuthority = new PublicKey(argv["newAuthority"]);
1180+
if (isMultisig && newAuthority.equals(tokenAuthority)) {
1181+
console.error(
1182+
`New authority matches token authority (${newAuthority.toBase58()}). To set mint authority as token authority, retry without --multisig`
1183+
);
1184+
process.exit(1);
1185+
}
1186+
if (!isMultisig && !newAuthority.equals(tokenAuthority)) {
1187+
console.error(
1188+
`New authority (${newAuthority.toBase58()}) does not match token authority (${tokenAuthority.toBase58()}). To set mint authority as a valid SPL Multisig, specify with --multisig`
1189+
);
1190+
process.exit(1);
1191+
}
1192+
1193+
// ensure manager is paused
1194+
if (!(await solanaNtt.isPaused())) {
1195+
console.error(
1196+
`Not paused. Set \`paused\` for Solana to \`true\` in ${path} and run \`ntt push\` to sync the changes on-chain. Then retry this command.`
1197+
);
1198+
process.exit(1);
1199+
}
1200+
1201+
// manager versions < 3.x.x have to call spl setAuthority instruction directly
1202+
if (major < 3) {
1203+
try {
1204+
await spl.setAuthority(
1205+
connection,
1206+
payerKeypair,
1207+
config.mint,
1208+
payerKeypair.publicKey,
1209+
spl.AuthorityType.MintTokens,
1210+
newAuthority,
1211+
[],
1212+
{ commitment: "finalized" },
1213+
config.tokenProgram
1214+
);
1215+
console.log(
1216+
`Token mint authority successfully updated to ${newAuthority.toBase58()}`
1217+
);
1218+
process.exit(0);
1219+
} catch (error) {
1220+
if (error instanceof Error) {
1221+
console.error(error.message);
1222+
} else if (error instanceof SendTransactionError) {
1223+
console.error(error.logs);
1224+
}
1225+
process.exit(1);
1226+
}
1227+
}
1228+
1229+
// use lut if configured
1230+
const luts: AddressLookupTableAccount[] = [];
1231+
try {
1232+
luts.push(await solanaNtt.getAddressLookupTable());
1233+
} catch {}
1234+
1235+
// send versioned transaction
1236+
try {
1237+
const latestBlockHash = await connection.getLatestBlockhash();
1238+
const messageV0 = new TransactionMessage({
1239+
payerKey: payerKeypair.publicKey,
1240+
instructions: [
1241+
await NTT.createAcceptTokenAuthorityInstruction(
1242+
solanaNtt.program,
1243+
config,
1244+
{
1245+
currentAuthority: payerKeypair.publicKey,
1246+
multisigTokenAuthority: isMultisig
1247+
? newAuthority
1248+
: undefined,
1249+
}
1250+
),
1251+
],
1252+
recentBlockhash: latestBlockHash.blockhash,
1253+
}).compileToV0Message(luts);
1254+
const vtx = new VersionedTransaction(messageV0);
1255+
vtx.sign([payerKeypair]);
1256+
const signature = await connection.sendTransaction(vtx, {});
1257+
await connection.confirmTransaction(
1258+
{
1259+
...latestBlockHash,
1260+
signature,
1261+
},
1262+
"finalized"
1263+
);
1264+
console.log(
1265+
`Token mint authority successfully updated to ${newAuthority.toBase58()}`
1266+
);
1267+
} catch (error) {
1268+
if (error instanceof Error) {
1269+
console.error(error.message);
1270+
} else if (error instanceof SendTransactionError) {
1271+
console.error(error.logs);
1272+
}
1273+
}
1274+
})
9661275
.demandCommand()
9671276
}
9681277
)
@@ -1415,7 +1724,7 @@ async function deploySolana<N extends Network, C extends SolanaChains>(
14151724
console.error(`Expected: ${expectedMintAuthority}`);
14161725
console.error(`Actual: ${actualMintAuthority}`);
14171726
console.error(`Set the mint authority to the program's token authority PDA with e.g.:`);
1418-
console.error(`spl-token authorize ${token} mint ${expectedMintAuthority}`);
1727+
console.error(`ntt solana set-mint-authority ${expectedMintAuthority}`);
14191728
process.exit(1);
14201729
}
14211730
}
@@ -1550,7 +1859,7 @@ async function missingConfigs(
15501859
// if it does, it means the LUT is missing or outdated. notice that
15511860
// we're not actually updating the LUT here, just checking if it's
15521861
// missing, so it's ok to use the 0 pubkey as the payer.
1553-
const updateLUT = solanaNtt.initializeOrUpdateLUT({ payer: new PublicKey(0) });
1862+
const updateLUT = solanaNtt.initializeOrUpdateLUT({ payer: new PublicKey(0), owner: new PublicKey(0) });
15541863
// check if async generator is non-empty
15551864
if (!(await updateLUT.next()).done) {
15561865
count++;
@@ -2149,4 +2458,4 @@ function nttVersion(): { version: string, commit: string, path: string, remote:
21492458
} catch {
21502459
return null;
21512460
}
2152-
}
2461+
}

0 commit comments

Comments
 (0)