diff --git a/clients/ts/src/bridge/generated/errors/bridge.ts b/clients/ts/src/bridge/generated/errors/bridge.ts index de8cd642..241431e2 100644 --- a/clients/ts/src/bridge/generated/errors/bridge.ts +++ b/clients/ts/src/bridge/generated/errors/bridge.ts @@ -84,6 +84,8 @@ export const BRIDGE_ERROR__INVALID_SCALER_EXPONENT = 0x319f; // 12703 export const BRIDGE_ERROR__MINT_IS_NOT_FROM_TOKEN2022 = 0x31a0; // 12704 /** MintIsNotWrappedTokenPda: Mint is not a valid wrapped token PDA */ export const BRIDGE_ERROR__MINT_IS_NOT_WRAPPED_TOKEN_PDA = 0x31a1; // 12705 +/** UriTooLong: Token metadata uri is too long */ +export const BRIDGE_ERROR__URI_TOO_LONG = 0x31a2; // 12706 /** InvalidThreshold: Threshold must be <= number of signers */ export const BRIDGE_ERROR__INVALID_THRESHOLD = 0x3200; // 12800 /** TooManySigners: Too many signers (max 32) */ @@ -150,6 +152,7 @@ export type BridgeError = | typeof BRIDGE_ERROR__UNAUTHORIZED_CONFIG_UPDATE | typeof BRIDGE_ERROR__UNAUTHORIZED_INITIALIZATION | typeof BRIDGE_ERROR__UNUSED_PROOF_ELEMENTS_REMAINING + | typeof BRIDGE_ERROR__URI_TOO_LONG | typeof BRIDGE_ERROR__ZERO_ADDRESS; let bridgeErrorMessages: Record | undefined; @@ -199,6 +202,7 @@ if (process.env.NODE_ENV !== 'production') { [BRIDGE_ERROR__UNAUTHORIZED_CONFIG_UPDATE]: `Unauthorized to update configuration`, [BRIDGE_ERROR__UNAUTHORIZED_INITIALIZATION]: `Only the upgrade authority can initialize the bridge`, [BRIDGE_ERROR__UNUSED_PROOF_ELEMENTS_REMAINING]: `Unused proof elements remaining`, + [BRIDGE_ERROR__URI_TOO_LONG]: `Token metadata uri is too long`, [BRIDGE_ERROR__ZERO_ADDRESS]: `Zero address`, }; } diff --git a/clients/ts/src/bridge/generated/instructions/index.ts b/clients/ts/src/bridge/generated/instructions/index.ts index 1345906c..d4538d29 100644 --- a/clients/ts/src/bridge/generated/instructions/index.ts +++ b/clients/ts/src/bridge/generated/instructions/index.ts @@ -41,3 +41,4 @@ export * from './setPauseStatus'; export * from './setWindowDuration'; export * from './transferGuardian'; export * from './wrapToken'; +export * from './wrapTokenV2'; diff --git a/clients/ts/src/bridge/generated/instructions/wrapToken.ts b/clients/ts/src/bridge/generated/instructions/wrapToken.ts index bb55c8cd..2af5e6ff 100644 --- a/clients/ts/src/bridge/generated/instructions/wrapToken.ts +++ b/clients/ts/src/bridge/generated/instructions/wrapToken.ts @@ -101,19 +101,9 @@ export type WrapTokenInstructionData = { name: string; /** The symbol/ticker of the token (e.g., "WBTC") */ symbol: string; - /** - * The 20-byte address of the corresponding token contract on Base (EVM address bytes). - * This allows the bridge to identify which Base token this Solana token represents. - */ + /** The 20-byte address of the corresponding token contract on Base (EVM address bytes). */ remoteToken: ReadonlyUint8Array; - /** - * The scaling exponent used to convert between token amounts on different chains. - * This handles cases where tokens have differing decimal precision on Base vs Solana. - * For example, when Base token has 18 decimals and the Solana wrapped mint has 9, - * this value conveys the decimal relationship so bridging logic can scale amounts. - * The exact conversion is performed by the EVM-side contract; Solana propagates this - * value but does not apply arithmetic with it. - */ + /** The scaling exponent used to convert between token amounts on different chains. */ scalerExponent: number; }; @@ -124,19 +114,9 @@ export type WrapTokenInstructionDataArgs = { name: string; /** The symbol/ticker of the token (e.g., "WBTC") */ symbol: string; - /** - * The 20-byte address of the corresponding token contract on Base (EVM address bytes). - * This allows the bridge to identify which Base token this Solana token represents. - */ + /** The 20-byte address of the corresponding token contract on Base (EVM address bytes). */ remoteToken: ReadonlyUint8Array; - /** - * The scaling exponent used to convert between token amounts on different chains. - * This handles cases where tokens have differing decimal precision on Base vs Solana. - * For example, when Base token has 18 decimals and the Solana wrapped mint has 9, - * this value conveys the decimal relationship so bridging logic can scale amounts. - * The exact conversion is performed by the EVM-side contract; Solana propagates this - * value but does not apply arithmetic with it. - */ + /** The scaling exponent used to convert between token amounts on different chains. */ scalerExponent: number; }; diff --git a/clients/ts/src/bridge/generated/instructions/wrapTokenV2.ts b/clients/ts/src/bridge/generated/instructions/wrapTokenV2.ts new file mode 100644 index 00000000..0c3aab6c --- /dev/null +++ b/clients/ts/src/bridge/generated/instructions/wrapTokenV2.ts @@ -0,0 +1,417 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + addDecoderSizePrefix, + addEncoderSizePrefix, + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU32Decoder, + getU32Encoder, + getU8Decoder, + getU8Encoder, + getUtf8Decoder, + getUtf8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type Codec, + type Decoder, + type Encoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, + type WritableSignerAccount, +} from '@solana/kit'; +import { BRIDGE_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const WRAP_TOKEN_V2_DISCRIMINATOR = new Uint8Array([ + 156, 173, 188, 64, 139, 131, 187, 136, +]); + +export function getWrapTokenV2DiscriminatorBytes() { + return fixEncoderSize(getBytesEncoder(), 8).encode( + WRAP_TOKEN_V2_DISCRIMINATOR + ); +} + +export type WrapTokenV2Instruction< + TProgram extends string = typeof BRIDGE_PROGRAM_ADDRESS, + TAccountPayer extends string | AccountMeta = string, + TAccountGasFeeReceiver extends string | AccountMeta = string, + TAccountMint extends string | AccountMeta = string, + TAccountBridge extends string | AccountMeta = string, + TAccountOutgoingMessage extends string | AccountMeta = string, + TAccountTokenProgram extends + | string + | AccountMeta = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', + TAccountSystemProgram extends + | string + | AccountMeta = '11111111111111111111111111111111', + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountPayer extends string + ? WritableSignerAccount & + AccountSignerMeta + : TAccountPayer, + TAccountGasFeeReceiver extends string + ? WritableAccount + : TAccountGasFeeReceiver, + TAccountMint extends string + ? WritableAccount + : TAccountMint, + TAccountBridge extends string + ? WritableAccount + : TAccountBridge, + TAccountOutgoingMessage extends string + ? WritableAccount + : TAccountOutgoingMessage, + TAccountTokenProgram extends string + ? ReadonlyAccount + : TAccountTokenProgram, + TAccountSystemProgram extends string + ? ReadonlyAccount + : TAccountSystemProgram, + ...TRemainingAccounts, + ] + >; + +export type WrapTokenV2InstructionData = { + discriminator: ReadonlyUint8Array; + outgoingMessageSalt: ReadonlyUint8Array; + decimals: number; + /** The human-readable name of the token (e.g., "Wrapped Bitcoin") */ + name: string; + /** The symbol/ticker of the token (e.g., "WBTC") */ + symbol: string; + /** + * URI pointing to off-chain JSON metadata holding the token's image and description. + * Token-2022 has no dedicated fields for either, so both are served from this document. + * + * NOTE: Deliberately excluded from [`PartialTokenMetadata::hash`]. See that method. + */ + uri: string; + /** + * The 20-byte address of the corresponding token contract on Base (EVM address bytes). + * This allows the bridge to identify which Base token this Solana token represents. + */ + remoteToken: ReadonlyUint8Array; + /** + * The scaling exponent used to convert between token amounts on different chains. + * This handles cases where tokens have differing decimal precision on Base vs Solana. + * For example, when Base token has 18 decimals and the Solana wrapped mint has 9, + * this value conveys the decimal relationship so bridging logic can scale amounts. + * The exact conversion is performed by the EVM-side contract; Solana propagates this + * value but does not apply arithmetic with it. + */ + scalerExponent: number; +}; + +export type WrapTokenV2InstructionDataArgs = { + outgoingMessageSalt: ReadonlyUint8Array; + decimals: number; + /** The human-readable name of the token (e.g., "Wrapped Bitcoin") */ + name: string; + /** The symbol/ticker of the token (e.g., "WBTC") */ + symbol: string; + /** + * URI pointing to off-chain JSON metadata holding the token's image and description. + * Token-2022 has no dedicated fields for either, so both are served from this document. + * + * NOTE: Deliberately excluded from [`PartialTokenMetadata::hash`]. See that method. + */ + uri: string; + /** + * The 20-byte address of the corresponding token contract on Base (EVM address bytes). + * This allows the bridge to identify which Base token this Solana token represents. + */ + remoteToken: ReadonlyUint8Array; + /** + * The scaling exponent used to convert between token amounts on different chains. + * This handles cases where tokens have differing decimal precision on Base vs Solana. + * For example, when Base token has 18 decimals and the Solana wrapped mint has 9, + * this value conveys the decimal relationship so bridging logic can scale amounts. + * The exact conversion is performed by the EVM-side contract; Solana propagates this + * value but does not apply arithmetic with it. + */ + scalerExponent: number; +}; + +export function getWrapTokenV2InstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', fixEncoderSize(getBytesEncoder(), 8)], + ['outgoingMessageSalt', fixEncoderSize(getBytesEncoder(), 32)], + ['decimals', getU8Encoder()], + ['name', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], + ['symbol', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], + ['uri', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], + ['remoteToken', fixEncoderSize(getBytesEncoder(), 20)], + ['scalerExponent', getU8Encoder()], + ]), + (value) => ({ ...value, discriminator: WRAP_TOKEN_V2_DISCRIMINATOR }) + ); +} + +export function getWrapTokenV2InstructionDataDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', fixDecoderSize(getBytesDecoder(), 8)], + ['outgoingMessageSalt', fixDecoderSize(getBytesDecoder(), 32)], + ['decimals', getU8Decoder()], + ['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], + ['symbol', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], + ['uri', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], + ['remoteToken', fixDecoderSize(getBytesDecoder(), 20)], + ['scalerExponent', getU8Decoder()], + ]); +} + +export function getWrapTokenV2InstructionDataCodec(): Codec< + WrapTokenV2InstructionDataArgs, + WrapTokenV2InstructionData +> { + return combineCodec( + getWrapTokenV2InstructionDataEncoder(), + getWrapTokenV2InstructionDataDecoder() + ); +} + +export type WrapTokenV2Input< + TAccountPayer extends string = string, + TAccountGasFeeReceiver extends string = string, + TAccountMint extends string = string, + TAccountBridge extends string = string, + TAccountOutgoingMessage extends string = string, + TAccountTokenProgram extends string = string, + TAccountSystemProgram extends string = string, +> = { + /** + * The account that pays for the transaction and all account creation costs. + * Must be mutable to deduct lamports for mint creation, metadata storage, and gas fees. + */ + payer: TransactionSigner; + /** The account that receives payment for the gas costs of registering the token on Base. */ + gasFeeReceiver: Address; + /** + * The new SPL Token-2022 mint being created for the wrapped token. + * - Uses PDA with token metadata hash and decimals for deterministic address + * - Mint authority set to itself (mint account) for controlled minting + * - Includes metadata pointer extension to store token information onchain + */ + mint: Address; + /** + * The main bridge state account that tracks cross-chain operations. + * Used to increment the nonce counter and manage EIP-1559 gas pricing. + * Must be mutable to update the nonce after creating the outgoing message. + */ + bridge: Address; + /** + * The outgoing message account that stores the cross-chain call to register + * the wrapped token on the Base blockchain. Contains the encoded function call + * with token address, local mint address, and scaling parameters. + */ + outgoingMessage: Address; + /** + * SPL Token-2022 program for creating the mint with metadata extensions. + * Required for initializing tokens with advanced features like metadata pointers. + */ + tokenProgram?: Address; + /** + * System program required for creating new accounts and transferring lamports. + * Used internally by Anchor for account initialization and rent payments. + */ + systemProgram?: Address; + outgoingMessageSalt: WrapTokenV2InstructionDataArgs['outgoingMessageSalt']; + decimals: WrapTokenV2InstructionDataArgs['decimals']; + name: WrapTokenV2InstructionDataArgs['name']; + symbol: WrapTokenV2InstructionDataArgs['symbol']; + uri: WrapTokenV2InstructionDataArgs['uri']; + remoteToken: WrapTokenV2InstructionDataArgs['remoteToken']; + scalerExponent: WrapTokenV2InstructionDataArgs['scalerExponent']; +}; + +export function getWrapTokenV2Instruction< + TAccountPayer extends string, + TAccountGasFeeReceiver extends string, + TAccountMint extends string, + TAccountBridge extends string, + TAccountOutgoingMessage extends string, + TAccountTokenProgram extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof BRIDGE_PROGRAM_ADDRESS, +>( + input: WrapTokenV2Input< + TAccountPayer, + TAccountGasFeeReceiver, + TAccountMint, + TAccountBridge, + TAccountOutgoingMessage, + TAccountTokenProgram, + TAccountSystemProgram + >, + config?: { programAddress?: TProgramAddress } +): WrapTokenV2Instruction< + TProgramAddress, + TAccountPayer, + TAccountGasFeeReceiver, + TAccountMint, + TAccountBridge, + TAccountOutgoingMessage, + TAccountTokenProgram, + TAccountSystemProgram +> { + // Program address. + const programAddress = config?.programAddress ?? BRIDGE_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + payer: { value: input.payer ?? null, isWritable: true }, + gasFeeReceiver: { value: input.gasFeeReceiver ?? null, isWritable: true }, + mint: { value: input.mint ?? null, isWritable: true }, + bridge: { value: input.bridge ?? null, isWritable: true }, + outgoingMessage: { value: input.outgoingMessage ?? null, isWritable: true }, + tokenProgram: { value: input.tokenProgram ?? null, isWritable: false }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + // Resolve default values. + if (!accounts.tokenProgram.value) { + accounts.tokenProgram.value = + 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' as Address<'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'>; + } + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = + '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>; + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.payer), + getAccountMeta(accounts.gasFeeReceiver), + getAccountMeta(accounts.mint), + getAccountMeta(accounts.bridge), + getAccountMeta(accounts.outgoingMessage), + getAccountMeta(accounts.tokenProgram), + getAccountMeta(accounts.systemProgram), + ], + data: getWrapTokenV2InstructionDataEncoder().encode( + args as WrapTokenV2InstructionDataArgs + ), + programAddress, + } as WrapTokenV2Instruction< + TProgramAddress, + TAccountPayer, + TAccountGasFeeReceiver, + TAccountMint, + TAccountBridge, + TAccountOutgoingMessage, + TAccountTokenProgram, + TAccountSystemProgram + >); +} + +export type ParsedWrapTokenV2Instruction< + TProgram extends string = typeof BRIDGE_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + /** + * The account that pays for the transaction and all account creation costs. + * Must be mutable to deduct lamports for mint creation, metadata storage, and gas fees. + */ + payer: TAccountMetas[0]; + /** The account that receives payment for the gas costs of registering the token on Base. */ + gasFeeReceiver: TAccountMetas[1]; + /** + * The new SPL Token-2022 mint being created for the wrapped token. + * - Uses PDA with token metadata hash and decimals for deterministic address + * - Mint authority set to itself (mint account) for controlled minting + * - Includes metadata pointer extension to store token information onchain + */ + mint: TAccountMetas[2]; + /** + * The main bridge state account that tracks cross-chain operations. + * Used to increment the nonce counter and manage EIP-1559 gas pricing. + * Must be mutable to update the nonce after creating the outgoing message. + */ + bridge: TAccountMetas[3]; + /** + * The outgoing message account that stores the cross-chain call to register + * the wrapped token on the Base blockchain. Contains the encoded function call + * with token address, local mint address, and scaling parameters. + */ + outgoingMessage: TAccountMetas[4]; + /** + * SPL Token-2022 program for creating the mint with metadata extensions. + * Required for initializing tokens with advanced features like metadata pointers. + */ + tokenProgram: TAccountMetas[5]; + /** + * System program required for creating new accounts and transferring lamports. + * Used internally by Anchor for account initialization and rent payments. + */ + systemProgram: TAccountMetas[6]; + }; + data: WrapTokenV2InstructionData; +}; + +export function parseWrapTokenV2Instruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedWrapTokenV2Instruction { + if (instruction.accounts.length < 7) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + payer: getNextAccount(), + gasFeeReceiver: getNextAccount(), + mint: getNextAccount(), + bridge: getNextAccount(), + outgoingMessage: getNextAccount(), + tokenProgram: getNextAccount(), + systemProgram: getNextAccount(), + }, + data: getWrapTokenV2InstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/ts/src/bridge/generated/programs/bridge.ts b/clients/ts/src/bridge/generated/programs/bridge.ts index b052b125..a2fabb7d 100644 --- a/clients/ts/src/bridge/generated/programs/bridge.ts +++ b/clients/ts/src/bridge/generated/programs/bridge.ts @@ -49,6 +49,7 @@ import { type ParsedSetWindowDurationInstruction, type ParsedTransferGuardianInstruction, type ParsedWrapTokenInstruction, + type ParsedWrapTokenV2Instruction, } from '../instructions'; export const BRIDGE_PROGRAM_ADDRESS = '' as Address<''>; @@ -173,6 +174,7 @@ export enum BridgeInstruction { SetWindowDuration, TransferGuardian, WrapToken, + WrapTokenV2, } export function identifyBridgeInstruction( @@ -564,6 +566,17 @@ export function identifyBridgeInstruction( ) { return BridgeInstruction.WrapToken; } + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode( + new Uint8Array([156, 173, 188, 64, 139, 131, 187, 136]) + ), + 0 + ) + ) { + return BridgeInstruction.WrapTokenV2; + } throw new Error( 'The provided instruction could not be identified as a bridge instruction.' ); @@ -674,4 +687,7 @@ export type ParsedBridgeInstruction = } & ParsedTransferGuardianInstruction) | ({ instructionType: BridgeInstruction.WrapToken; - } & ParsedWrapTokenInstruction); + } & ParsedWrapTokenInstruction) + | ({ + instructionType: BridgeInstruction.WrapTokenV2; + } & ParsedWrapTokenV2Instruction); diff --git a/scripts/src/commands/sol/generate-client.handler.ts b/scripts/src/commands/sol/generate-client.handler.ts index d2149045..f5b27662 100644 --- a/scripts/src/commands/sol/generate-client.handler.ts +++ b/scripts/src/commands/sol/generate-client.handler.ts @@ -8,6 +8,8 @@ import { z } from "zod"; import { logger } from "@internal/logger"; import { findGitRoot } from "@internal/utils"; +const TOKEN_2022_PROGRAM_ADDRESS = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"; + export const argsSchema = z.object({ program: z .enum(["bridge", "base-relayer"], { @@ -42,6 +44,15 @@ export async function handleGenerateClient( logger.info("Instantiating Codama..."); const idl = rootNodeFromAnchor(require(idlPath)); const codama = c.createFromRoot(idl); + codama.update( + c.setInstructionAccountDefaultValuesVisitor([ + { + instruction: "wrapTokenV2", + account: "tokenProgram", + defaultValue: c.publicKeyValueNode(TOKEN_2022_PROGRAM_ADDRESS), + }, + ]) + ); logger.info("Rendering TypeScript client..."); codama.accept(renderJavaScriptVisitor(clientOutputPath));