Skip to content

The default Range message format is {timestamp}_{pubkey}. You can extend this format to include additional verified data that’s checked on-chain.

{timestamp}_{pubkey}

Example: 1704067200_7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU

Add additional fields separated by underscores:

{timestamp}_{pubkey}_{field1}_{field2}_{...}

Examples:

  • 1704067200_7xKXt..._1000 - Amount limit
  • 1704067200_7xKXt..._transfer_vault1 - Action and target
  • 1704067200_7xKXt..._kyc_2 - KYC level

Authorize transfers up to a specific amount:

// Backend
const maxAmount = 1000;
const message = `${timestamp}_${pubkey}_${maxAmount}`;
const signature = sign(message, rangeSignerKeypair);
// On-chain
fn verify_with_amount(
ctx: Context<VerifyWithAmount>,
signature: [u8; 64],
message: Vec<u8>,
requested_amount: u64,
) -> Result<()> {
// Standard Range verification first
verify_range_signature(&ctx, &signature, &message)?;
// Parse extended message
let message_str = String::from_utf8(message)
.map_err(|_| ErrorCode::InvalidMessage)?;
let parts: Vec<&str> = message_str.split('_').collect();
require!(parts.len() >= 3, ErrorCode::InvalidMessageFormat);
// Extract and validate amount
let max_amount: u64 = parts[2].parse()
.map_err(|_| ErrorCode::InvalidAmount)?;
require!(
requested_amount <= max_amount,
ErrorCode::AmountExceedsLimit
);
// Proceed with transfer
Ok(())
}

Authorize specific actions:

// Backend - only sign for allowed actions
const allowedActions = ['deposit', 'withdraw', 'stake'];
if (!allowedActions.includes(requestedAction)) {
return res.status(403).json({ error: 'Action not allowed' });
}
const message = `${timestamp}_${pubkey}_${requestedAction}`;
// On-chain
#[derive(PartialEq)]
enum Action {
Deposit,
Withdraw,
Stake,
}
fn verify_action(
ctx: Context<VerifyAction>,
signature: [u8; 64],
message: Vec<u8>,
action: Action,
) -> Result<()> {
verify_range_signature(&ctx, &signature, &message)?;
let message_str = String::from_utf8(message)?;
let parts: Vec<&str> = message_str.split('_').collect();
let signed_action = match parts.get(2) {
Some(&"deposit") => Action::Deposit,
Some(&"withdraw") => Action::Withdraw,
Some(&"stake") => Action::Stake,
_ => return Err(ErrorCode::InvalidAction.into()),
};
require!(signed_action == action, ErrorCode::ActionMismatch);
Ok(())
}

Different verification levels for different access:

// Backend - determine KYC level from compliance check
const kycLevel = await getKYCLevel(pubkey); // 0, 1, 2, 3
const message = `${timestamp}_${pubkey}_kyc_${kycLevel}`;
// On-chain
fn verify_kyc_level(
ctx: Context<VerifyKYC>,
signature: [u8; 64],
message: Vec<u8>,
required_level: u8,
) -> Result<()> {
verify_range_signature(&ctx, &signature, &message)?;
let message_str = String::from_utf8(message)?;
let parts: Vec<&str> = message_str.split('_').collect();
// Expect: timestamp_pubkey_kyc_level
require!(parts.len() >= 4, ErrorCode::InvalidMessageFormat);
require!(parts[2] == "kyc", ErrorCode::NotKYCMessage);
let user_level: u8 = parts[3].parse()
.map_err(|_| ErrorCode::InvalidKYCLevel)?;
require!(
user_level >= required_level,
ErrorCode::InsufficientKYCLevel
);
Ok(())
}

Combine multiple fields:

// Backend
const message = `${timestamp}_${pubkey}_${action}_${amount}_${target}`;
// Example: 1704067200_7xKXt..._transfer_1000_vault1
// On-chain
#[derive(Debug)]
struct ParsedMessage {
timestamp: i64,
pubkey: Pubkey,
action: String,
amount: u64,
target: String,
}
fn parse_extended_message(message: &[u8]) -> Result<ParsedMessage> {
let message_str = String::from_utf8(message.to_vec())
.map_err(|_| ErrorCode::InvalidMessage)?;
let parts: Vec<&str> = message_str.split('_').collect();
require!(parts.len() == 5, ErrorCode::InvalidMessageFormat);
Ok(ParsedMessage {
timestamp: parts[0].parse().map_err(|_| ErrorCode::InvalidTimestamp)?,
pubkey: Pubkey::from_str(parts[1]).map_err(|_| ErrorCode::InvalidPubkey)?,
action: parts[2].to_string(),
amount: parts[3].parse().map_err(|_| ErrorCode::InvalidAmount)?,
target: parts[4].to_string(),
})
}
types.ts
interface ExtendedMessage {
timestamp: number;
pubkey: string;
action: 'transfer' | 'stake' | 'unstake';
amount?: number;
target?: string;
}
function encodeMessage(msg: ExtendedMessage): string {
const parts = [msg.timestamp, msg.pubkey, msg.action];
if (msg.amount !== undefined) parts.push(msg.amount.toString());
if (msg.target !== undefined) parts.push(msg.target);
return parts.join('_');
}
app.post('/api/verify-extended', async (req, res) => {
const { pubkey, action, amount, target } = req.body;
// Validate action is allowed
if (!isActionAllowed(pubkey, action, amount)) {
return res.status(403).json({ error: 'Action not permitted' });
}
const message = encodeMessage({
timestamp: Math.floor(Date.now() / 1000),
pubkey,
action,
amount,
target,
});
const signature = sign(message, rangeSignerKeypair);
res.json({ signature, message });
});
async function verifyExtendedAction(
action: string,
amount: number,
target: string
) {
// Get signed message from backend
const { signature, message } = await fetch('/api/verify-extended', {
method: 'POST',
body: JSON.stringify({ pubkey, action, amount, target }),
}).then(r => r.json());
// Build and send transaction
const instruction = await buildVerifyExtendedInstruction({
signer: publicKey,
admin: settingsAdmin,
signature: Buffer.from(signature, 'base64'),
message: Buffer.from(message),
action,
amount,
target,
});
await sendTransaction(new Transaction().add(instruction));
}

If your values might contain underscores:

// Use | as delimiter
const message = `${timestamp}|${pubkey}|${action}|${amount}`;
let parts: Vec<&str> = message_str.split('|').collect();

For complex structured data:

const payload = JSON.stringify({
timestamp,
pubkey,
action,
params: { amount, target, metadata: {...} },
});
const message = payload;