From cf19dd5e52ec805288ce9992b8b211a31238743f Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Wed, 16 Sep 2026 17:23:24 -0400 Subject: [PATCH] feat(scripts): accept a metadata uri when wrapping a token Switches the wrap-token command to `wrap_token_v2` and adds an optional `--uri`, validated against `MAX_URI_LEN` read from the IDL. The length check counts UTF-8 bytes to match the onchain check. The metadata hash preimage is deliberately left alone, mirroring `PartialTokenMetadata::hash`, so the derived mint still matches the program's. Co-authored-by: Cursor --- .../solana-to-base/wrap-token.command.ts | 19 +++++++++++++++ .../solana-to-base/wrap-token.handler.ts | 23 +++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.command.ts b/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.command.ts index 0d1e0a8a..53d76e18 100644 --- a/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.command.ts +++ b/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.command.ts @@ -8,6 +8,7 @@ import { getOrPromptFilePath, getOrPromptDeployEnv, validateAndExecute, + getInteractiveInput, getInteractiveSelect, } from "@internal/utils/cli"; import { argsSchema, handleWrapToken } from "./wrap-token.handler"; @@ -17,6 +18,7 @@ type CommanderOptions = { decimals?: string; name?: string; symbol?: string; + uri?: string; remoteToken?: string; scalerExponent?: string; payerKp?: string; @@ -50,6 +52,19 @@ async function collectInteractiveOptions( "wERC20" ); + if (opts.uri === undefined) { + opts.uri = await getInteractiveInput( + "Enter token metadata uri, which serves the icon and description (leave blank for none)", + "https://example.com/token.json", + (input) => { + const result = argsSchema.shape.uri.safeParse(input.trim()); + return result.success + ? undefined + : result.error.issues[0]?.message || "Invalid uri"; + } + ); + } + if (!opts.remoteToken) { const remoteToken = await getInteractiveSelect({ message: "Select remote token:", @@ -102,6 +117,10 @@ export const wrapTokenCommand = new Command("wrap-token") .option("--decimals ", "Token decimals") .option("--name ", "Token name") .option("--symbol ", "Token symbol") + .option( + "--uri ", + "Uri of the off-chain JSON metadata serving the token icon and description" + ) .option( "--remote-token ", "Remote token address: 'constant-erc20', 'constant-eth', or custom address" diff --git a/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.handler.ts b/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.handler.ts index fd4dc02f..2fcd9005 100644 --- a/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.handler.ts +++ b/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.handler.ts @@ -13,8 +13,8 @@ import { keccak256, toBytes } from "viem"; import { fetchBridge, - getWrapTokenInstruction, - type WrapTokenInstructionDataArgs, + getWrapTokenV2Instruction, + type WrapTokenV2InstructionDataArgs, } from "@base/bridge/bridge"; import { logger } from "@internal/logger"; @@ -49,6 +49,16 @@ export const argsSchema = z.object({ .nonempty("Token name cannot be empty") .default("Wrapped ERC20"), symbol: z.string().nonempty("Token symbol cannot be empty").default("wERC20"), + uri: z + .string() + .refine((val) => val.trim() === "" || URL.canParse(val.trim()), { + message: "Token uri must be a valid URL", + }) + .refine( + (val) => Buffer.byteLength(val, "utf8") <= getIdlConstant("MAX_URI_LEN"), + { message: "Token uri is too long" } + ) + .default(""), remoteToken: z.union([ z.literal("constant-erc20"), z.literal("constant-eth"), @@ -98,11 +108,12 @@ export async function handleWrapToken(args: Args): Promise { logger.info(`Outgoing message: ${outgoingMessage}`); // Instruction arguments - const instructionArgs: WrapTokenInstructionDataArgs = { + const instructionArgs: WrapTokenV2InstructionDataArgs = { outgoingMessageSalt: salt, decimals: args.decimals, name: args.name, symbol: args.symbol, + uri: args.uri, remoteToken: toBytes(remoteToken), scalerExponent: args.scalerExponent, }; @@ -115,7 +126,9 @@ export async function handleWrapToken(args: Args): Promise { instructionArgs.symbol.length ); - // Calculate metadata hash + // Calculate metadata hash. + // NOTE: `uri` is deliberately absent, mirroring `PartialTokenMetadata::hash` onchain. Adding it + // here would derive a mint the program does not recognize. const metadataHash = keccak256( Buffer.concat([ Buffer.from(nameLengthLeBytes), @@ -148,7 +161,7 @@ export async function handleWrapToken(args: Args): Promise { // Build wrap token instruction const ixs: Instruction[] = [ - getWrapTokenInstruction( + getWrapTokenV2Instruction( { // Accounts payer,