RevibaseDeveloper

Verify on-chain

Require an accessory tap inside your own Solana program. Your instruction CPIs verify, which checks the tap's signature against a message you choose; then you read the token's linkedWallet.

Use this when the proof must be enforced on-chain. For a website login, use the Quickstart.

pnpm add phygital-token-sdk @solana/kit cargo add phygital-token-client

1. Tap and build the transaction

Hash the message your program checks, prompt the tap, and prepend secp256r1_verify to your transaction.

import { authenticatePasskeyForSecp256r1Verify, buildMessageHash, buildSecp256r1VerifyInstruction, } from "phygital-token-sdk"; // Bind the action: whatever your program will recompute and compare. const messageHash = buildMessageHash(message); const response = await authenticatePasskeyForSecp256r1Verify({ rpc, messageHash, }); // user taps const { secp256r1VerifyInstruction, phygitalTokenPda, secp256r1VerifyArgs } = await buildSecp256r1VerifyInstruction(response); // Transaction: [secp256r1VerifyInstruction, yourInstruction] // Pass phygitalTokenPda (writable) and secp256r1VerifyArgs to yourInstruction.
  • Pass rpc, or allowList (a Set of base64url compressed public keys) to also reject any accessory outside it.
  • Native reader: pass transceive. rpId and origin are optional; set origin if your program checks expected_origins.
  • secp256r1VerifyInstruction must sit immediately before your instruction; otherwise pass its relative index as the third argument to buildSecp256r1VerifyInstruction.

2. CPI verify in your program

use phygital_token_client::{ accounts::PhygitalToken, instructions::VerifyCpiBuilder, }; let program = ctx.accounts.phygital_token_program.to_account_info(); let phygital_token = ctx.accounts.phygital_token.to_account_info(); // mut let instructions_sysvar = ctx.accounts.instructions_sysvar.to_account_info(); VerifyCpiBuilder::new(&program) .phygital_token(&phygital_token) .instructions_sysvar(&instructions_sysvar) .secp256r1_verify_args(args.secp256r1_verify_args) .message_hash(message_hash) // recompute it here from your instruction's inputs .expected_origins(vec!["https://app.example".into()]) // optional .invoke()?; let token = PhygitalToken::try_from(&phygital_token) .map_err(|_| error!(MyError::InvalidAccessory))?; // Your token-type policy: 0 = Permanent, 1 = Bearer, 2 = Controlled. require!(ACCEPTED_TYPES.contains(&token.token_type), MyError::TypeNotAccepted); require!(token.linked_wallet == expected_wallet, MyError::WrongWallet);

verify checks the signature, that the passkey matches this token, and that the sign count is newer than the last seen, so each tap works once.

What goes in the message

verify checks the message as given and adds no freshness, so include:

  • What the tap authorizes: your instruction's arguments and accounts, so a tap for one action can't authorize another.
  • When: a recent slot hash checked against the SlotHashes sysvar, so an unused tap can't be held for later.

Set expected_origins to your site's origins to reject taps collected elsewhere (expected_rp_id is also optional).